// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.dialogs;

import static org.openstreetmap.josm.tools.I18n.marktr;
import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.DefaultListSelectionModel;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JToggleButton.ToggleButtonModel;
import javax.swing.ListSelectionModel;
import javax.swing.SingleSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;

import org.openstreetmap.josm.actions.ExtensionFileFilter;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.actions.PreferencesAction;
import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
import org.openstreetmap.josm.gui.ExtendedDialog;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.MapPaintStylesUpdateListener;
import org.openstreetmap.josm.gui.mappaint.StyleSettingGroupGui;
import org.openstreetmap.josm.gui.mappaint.StyleSource;
import org.openstreetmap.josm.gui.mappaint.loader.MapPaintStyleLoader;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
import org.openstreetmap.josm.gui.util.FileFilterAllFiles;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.gui.util.StayOpenPopupMenu;
import org.openstreetmap.josm.gui.util.TableHelper;
import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
import org.openstreetmap.josm.gui.widgets.FileChooserManager;
import org.openstreetmap.josm.gui.widgets.HtmlPanel;
import org.openstreetmap.josm.gui.widgets.JosmTextArea;
import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
import org.openstreetmap.josm.gui.widgets.ScrollableTable;
import org.openstreetmap.josm.tools.ColorHelper;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageOverlay;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
import org.openstreetmap.josm.tools.InputMapUtils;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.Utils;

/**
 * Dialog to configure the map painting style.
 * @since 3843
 */
public class MapPaintDialog extends ToggleDialog {

    protected ScrollableTable tblStyles;
    protected StylesModel model;
    protected final DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();

    protected OnOffAction onoffAction;
    protected ReloadAction reloadAction;
    protected MoveUpDownAction upAction;
    protected MoveUpDownAction downAction;
    protected JCheckBox cbWireframe;

    /**
     * Action that opens the map paint preferences.
     */
    public static final JosmAction PREFERENCE_ACTION = PreferencesAction.forPreferenceTab(
            tr("Map paint preferences..."), null, MapPaintPreference.class, /* ICON */ "dialogs/mappaintpreference");

    /**
     * Constructs a new {@code MapPaintDialog}.
     */
    public MapPaintDialog() {
        super(tr("Map Paint Styles"), "mapstyle", tr("configure the map painting style"),
                Shortcut.registerShortcut("subwindow:mappaint", tr("Windows: {0}", tr("Map Paint Styles")),
                        KeyEvent.VK_M, Shortcut.ALT_SHIFT), 150, false, MapPaintPreference.class);
        build();
    }

    protected void build() {
        model = new StylesModel();

        cbWireframe = new JCheckBox();
        JLabel wfLabel = new JLabel(tr("Wireframe View"), ImageProvider.get("dialogs/mappaint", "wireframe_small"), SwingConstants.HORIZONTAL);
        wfLabel.setFont(wfLabel.getFont().deriveFont(Font.PLAIN));
        wfLabel.setLabelFor(cbWireframe);

        cbWireframe.setModel(new ToggleButtonModel() {
            @Override
            public void setSelected(boolean b) {
                super.setSelected(b);
                tblStyles.setEnabled(!b);
                onoffAction.updateEnabledState();
                upAction.updateEnabledState();
                downAction.updateEnabledState();
            }
        });
        cbWireframe.addActionListener(e -> MainApplication.getMenu().wireFrameToggleAction.actionPerformed(null));
        cbWireframe.setBorder(new EmptyBorder(new Insets(1, 1, 1, 1)));

        tblStyles = new ScrollableTable(model);
        tblStyles.setSelectionModel(selectionModel);
        tblStyles.addMouseListener(new PopupMenuHandler());
        tblStyles.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
        tblStyles.setBackground(UIManager.getColor("Panel.background"));
        tblStyles.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        tblStyles.setTableHeader(null);
        tblStyles.getColumnModel().getColumn(0).setMaxWidth(1);
        tblStyles.getColumnModel().getColumn(0).setResizable(false);
        tblStyles.getColumnModel().getColumn(0).setCellRenderer(new MyCheckBoxRenderer());
        tblStyles.getColumnModel().getColumn(1).setCellRenderer(new StyleSourceRenderer());
        tblStyles.setShowGrid(false);
        tblStyles.setIntercellSpacing(new Dimension(0, 0));

        JPanel p = new JPanel(new GridBagLayout());
        p.add(cbWireframe, GBC.std(0, 0));
        p.add(wfLabel, GBC.std(1, 0).weight(1, 0));
        p.add(tblStyles, GBC.std(0, 1).span(2).fill());

        reloadAction = new ReloadAction();
        onoffAction = new OnOffAction();
        upAction = new MoveUpDownAction(false);
        downAction = new MoveUpDownAction(true);
        selectionModel.addListSelectionListener(onoffAction);
        selectionModel.addListSelectionListener(reloadAction);
        selectionModel.addListSelectionListener(upAction);
        selectionModel.addListSelectionListener(downAction);

        // Toggle style on Enter and Spacebar
        InputMapUtils.addEnterAction(tblStyles, onoffAction);
        InputMapUtils.addSpacebarAction(tblStyles, onoffAction);

        createLayout(p, true, Arrays.asList(
                new SideButton(onoffAction, false),
                new SideButton(upAction, false),
                new SideButton(downAction, false),
                new SideButton(PREFERENCE_ACTION, false)
        ));
    }

    @Override
    public void showNotify() {
        MapPaintStyles.addMapPaintStylesUpdateListener(model);
        model.mapPaintStylesUpdated();
        MainApplication.getMenu().wireFrameToggleAction.addButtonModel(cbWireframe.getModel());
    }

    @Override
    public void hideNotify() {
        MainApplication.getMenu().wireFrameToggleAction.removeButtonModel(cbWireframe.getModel());
        MapPaintStyles.removeMapPaintStylesUpdateListener(model);
    }

    protected class StylesModel extends AbstractTableModel implements MapPaintStylesUpdateListener {

        private final Class<?>[] columnClasses = {Boolean.class, StyleSource.class};

        private transient List<StyleSource> data = new ArrayList<>();

        /**
         * Constructs a new {@code StylesModel}.
         */
        public StylesModel() {
            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
        }

        private StyleSource getRow(int i) {
            return data.get(i);
        }

        @Override
        public int getColumnCount() {
            return 2;
        }

        @Override
        public int getRowCount() {
            return data.size();
        }

        @Override
        public Object getValueAt(int row, int column) {
            if (column == 0)
                return getRow(row).active;
            else
                return getRow(row);
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return column == 0;
        }

        @Override
        public Class<?> getColumnClass(int column) {
            return columnClasses[column];
        }

        @Override
        public void setValueAt(Object aValue, int row, int column) {
            if (row < 0 || row >= getRowCount() || aValue == null)
                return;
            if (column == 0) {
                MapPaintStyles.toggleStyleActive(row);
            }
        }

        /**
         * Make sure the first of the selected entry is visible in the
         * views of this model.
         */
        public void ensureSelectedIsVisible() {
            int index = selectionModel.getMinSelectionIndex();
            if (index < 0)
                return;
            if (index >= getRowCount())
                return;
            tblStyles.scrollToVisible(index, 0);
            tblStyles.repaint();
        }

        @Override
        public void mapPaintStylesUpdated() {
            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
            fireTableDataChanged();
            tblStyles.repaint();
        }

        @Override
        public void mapPaintStyleEntryUpdated(int idx) {
            data = new ArrayList<>(MapPaintStyles.getStyles().getStyleSources());
            fireTableRowsUpdated(idx, idx);
            tblStyles.repaint();
        }
    }

    private class MyCheckBoxRenderer extends JCheckBox implements TableCellRenderer {

        /**
         * Constructs a new {@code MyCheckBoxRenderer}.
         */
        MyCheckBoxRenderer() {
            setHorizontalAlignment(SwingConstants.CENTER);
            setVerticalAlignment(SwingConstants.CENTER);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (value == null)
                return this;
            boolean b = (Boolean) value;
            setSelected(b);
            setEnabled(!cbWireframe.isSelected());
            return this;
        }
    }

    private final class StyleSourceRenderer extends DefaultTableCellRenderer {
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (value == null)
                return this;
            StyleSource s = (StyleSource) value;
            JLabel label = (JLabel) super.getTableCellRendererComponent(table,
                    s.getDisplayString(), isSelected, hasFocus, row, column);
            label.setIcon(s.getIcon());
            label.setToolTipText(s.getToolTipText());
            label.setEnabled(!cbWireframe.isSelected());
            return label;
        }
    }

    protected class OnOffAction extends JosmAction implements ListSelectionListener {
        /**
         * Constructs a new {@code OnOffAction}.
         */
        public OnOffAction() {
            super(tr("On/Off"), "apply", tr("Turn selected styles on or off"),
                    Shortcut.registerShortcut("map:paint:style:on_off", tr("Filter: Add"), KeyEvent.VK_UNDEFINED, Shortcut.NONE),
                    false, false);
            updateEnabledState();
        }

        @Override
        protected void updateEnabledState() {
            setEnabled(!cbWireframe.isSelected() && tblStyles.getSelectedRowCount() > 0);
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int[] pos = tblStyles.getSelectedRows();
            MapPaintStyles.toggleStyleActive(pos);
            TableHelper.setSelectedIndices(selectionModel, Arrays.stream(pos));
        }
    }

    /**
     * The action to move down the currently selected entries in the list.
     */
    protected class MoveUpDownAction extends JosmAction implements ListSelectionListener {

        private final int increment;

        /**
         * Constructs a new {@code MoveUpDownAction}.
         * @param isDown {@code true} to move the entry down, {@code false} to move it up
         */
        public MoveUpDownAction(boolean isDown) {
            super(isDown ? tr("Down") : tr("Up"), "dialogs/" + (isDown ? "down" : "up"),
                    isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."),
                    isDown ? Shortcut.registerShortcut("map:paint:style:down", tr("Map Paint Styles: Move selected entry down"),
                            KeyEvent.VK_UNDEFINED, Shortcut.NONE)
                    : Shortcut.registerShortcut("map:paint:style:up", tr("Map Paint Styles: Move selected entry up"),
                            KeyEvent.VK_UNDEFINED, Shortcut.NONE),
                    false, false);
            increment = isDown ? 1 : -1;
            updateEnabledState();
        }

        @Override
        public void updateEnabledState() {
            int[] sel = tblStyles.getSelectedRows();
            setEnabled(!cbWireframe.isSelected() && MapPaintStyles.canMoveStyles(sel, increment));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int[] sel = tblStyles.getSelectedRows();
            MapPaintStyles.moveStyles(sel, increment);
            TableHelper.setSelectedIndices(selectionModel, Arrays.stream(sel).map(row -> row + increment));
            model.ensureSelectedIsVisible();
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }
    }

    protected class ReloadAction extends AbstractAction implements ListSelectionListener {
        /**
         * Constructs a new {@code ReloadAction}.
         */
        public ReloadAction() {
            putValue(NAME, tr("Reload from file"));
            putValue(SHORT_DESCRIPTION, tr("reload selected styles from file"));
            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
            setEnabled(getEnabledState());
        }

        protected boolean getEnabledState() {
            if (cbWireframe.isSelected())
                return false;
            int[] pos = tblStyles.getSelectedRows();
            return pos.length > 0 && Arrays.stream(pos).allMatch(i -> model.getRow(i).isLocal());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            setEnabled(getEnabledState());
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            final int[] rows = tblStyles.getSelectedRows();
            MapPaintStyleLoader.reloadStyles(rows);
            MainApplication.worker.submit(() -> SwingUtilities.invokeLater(() ->
                    TableHelper.setSelectedIndices(selectionModel, Arrays.stream(rows))));
        }
    }

    protected class SaveAsAction extends AbstractAction {

        /**
         * Constructs a new {@code SaveAsAction}.
         */
        public SaveAsAction() {
            putValue(NAME, tr("Save as..."));
            putValue(SHORT_DESCRIPTION, tr("Save a copy of this Style to file and add it to the list"));
            new ImageProvider("copy").getResource().attachImageIcon(this);
            setEnabled(tblStyles.getSelectedRows().length == 1);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
            if (sel < 0 || sel >= model.getRowCount())
                return;
            final StyleSource s = model.getRow(sel);

            FileChooserManager fcm = new FileChooserManager(false, "mappaint.clone-style.lastDirectory", Utils.getSystemProperty("user.home"));
            String suggestion = fcm.getInitialDirectory() + File.separator + s.getFileNamePart();

            FileFilter ff;
            if (s instanceof MapCSSStyleSource) {
                ff = new ExtensionFileFilter("mapcss,css,zip", "mapcss", tr("Map paint style file (*.mapcss, *.zip)"));
            } else {
                ff = new ExtensionFileFilter("xml,zip", "xml", tr("Map paint style file (*.xml, *.zip)"));
            }
            fcm.createFileChooser(false, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY)
                    .getFileChooser().setSelectedFile(new File(suggestion));
            AbstractFileChooser fc = fcm.openFileChooser();
            if (fc == null)
                return;
            MainApplication.worker.submit(new SaveToFileTask(s, fc.getSelectedFile()));
        }

        private class SaveToFileTask extends PleaseWaitRunnable {
            private final StyleSource s;
            private final File file;

            private boolean canceled;
            private boolean error;

            SaveToFileTask(StyleSource s, File file) {
                super(tr("Reloading style sources"));
                this.s = s;
                this.file = file;
            }

            @Override
            protected void cancel() {
                canceled = true;
            }

            @Override
            protected void realRun() {
                getProgressMonitor().indeterminateSubTask(
                        tr("Save style ''{0}'' as ''{1}''", s.getDisplayString(), file.getPath()));
                try {
                    try (InputStream in = s.getSourceInputStream()) {
                        Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
                    }
                } catch (IOException e) {
                    Logging.warn(e);
                    error = true;
                }
            }

            @Override
            protected void finish() {
                SwingUtilities.invokeLater(() -> {
                    if (!error && !canceled) {
                        SourceEntry se = new SourceEntry(s);
                        se.url = file.getPath();
                        MapPaintStyles.addStyle(se);
                        tblStyles.getSelectionModel().setSelectionInterval(model.getRowCount() - 1, model.getRowCount() - 1);
                        model.ensureSelectedIsVisible();
                    }
                });
            }
        }
    }

    /**
     * Displays information about selected paint style in a new dialog.
     */
    protected class InfoAction extends AbstractAction {

        private boolean errorsTabLoaded;
        private boolean warningsTabLoaded;
        private boolean sourceTabLoaded;

        /**
         * Constructs a new {@code InfoAction}.
         */
        public InfoAction() {
            putValue(NAME, tr("Info"));
            putValue(SHORT_DESCRIPTION, tr("view meta information, error log and source definition"));
            new ImageProvider("info").getResource().attachImageIcon(this);
            setEnabled(tblStyles.getSelectedRows().length == 1);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
            if (sel < 0 || sel >= model.getRowCount())
                return;
            final StyleSource s = model.getRow(sel);
            ExtendedDialog info = new ExtendedDialog(MainApplication.getMainFrame(), tr("Map Style info"), tr("Close"));
            info.setPreferredSize(new Dimension(600, 400));
            info.setButtonIcons("ok");

            final JTabbedPane tabs = new JTabbedPane();

            JLabel lblInfo = new JLabel(tr("Info"));
            lblInfo.setLabelFor(tabs.add("Info", buildInfoPanel(s)));
            lblInfo.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
            tabs.setTabComponentAt(0, lblInfo);

            final JPanel pErrors = addErrorOrWarningTab(tabs, lblInfo,
                    s.getErrors(), marktr("Errors"), 1, ImageProvider.get("misc", "error"));
            final JPanel pWarnings = addErrorOrWarningTab(tabs, lblInfo,
                    s.getWarnings(), marktr("Warnings"), 2, ImageProvider.get("warning-small"));

            final JPanel pSource = new JPanel(new GridBagLayout());
            JLabel lblSource = new JLabel(tr("Source"));
            lblSource.setLabelFor(tabs.add("Source", pSource));
            lblSource.setFont(lblSource.getFont().deriveFont(Font.PLAIN));
            tabs.setTabComponentAt(3, lblSource);

            tabs.getModel().addChangeListener(e1 -> {
                if (!errorsTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 1) {
                    errorsTabLoaded = true;
                    buildErrorsOrWarningPanel(s.getErrors(), pErrors);
                }
                if (!warningsTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 2) {
                    warningsTabLoaded = true;
                    buildErrorsOrWarningPanel(s.getWarnings(), pWarnings);
                }
                if (!sourceTabLoaded && ((SingleSelectionModel) e1.getSource()).getSelectedIndex() == 3) {
                    sourceTabLoaded = true;
                    buildSourcePanel(s, pSource);
                }
            });
            info.setContent(tabs, false);
            info.showDialog();
        }

        private JPanel addErrorOrWarningTab(final JTabbedPane tabs, JLabel lblInfo,
                Collection<?> items, String title, int pos, ImageIcon icon) {
            final JPanel pErrors = new JPanel(new GridBagLayout());
            tabs.add(title, pErrors);
            if (items.isEmpty()) {
                JLabel lblErrors = new JLabel(tr(title));
                lblErrors.setLabelFor(pErrors);
                lblErrors.setFont(lblInfo.getFont().deriveFont(Font.PLAIN));
                lblErrors.setEnabled(false);
                tabs.setTabComponentAt(pos, lblErrors);
                tabs.setEnabledAt(pos, false);
            } else {
                JLabel lblErrors = new JLabel(tr(title), icon, SwingConstants.HORIZONTAL);
                lblErrors.setLabelFor(pErrors);
                tabs.setTabComponentAt(pos, lblErrors);
            }
            return pErrors;
        }

        private JPanel buildInfoPanel(StyleSource s) {
            JPanel p = new JPanel(new GridBagLayout());
            StringBuilder text = new StringBuilder("<table cellpadding=3>");
            text.append(tableRow(tr("Title:"), s.getDisplayString()));
            if (s.url.startsWith("http://") || s.url.startsWith("https://")) {
                text.append(tableRow(tr("URL:"), s.url));
            } else if (s.url.startsWith("resource://")) {
                text.append(tableRow(tr("Built-in Style, internal path:"), s.url));
            } else {
                text.append(tableRow(tr("Path:"), s.url));
            }
            if (s.icon != null) {
                text.append(tableRow(tr("Icon:"), s.icon));
            }
            if (s.getBackgroundColorOverride() != null) {
                text.append(tableRow(tr("Background:"), ColorHelper.color2html(s.getBackgroundColorOverride())));
            }
            text.append(tableRow(tr("Style is currently active?"), s.active ? tr("Yes") : tr("No")))
                .append("</table>");
            p.add(new JScrollPane(new HtmlPanel(text.toString())), GBC.eol().fill(GridBagConstraints.BOTH));
            return p;
        }

        private String tableRow(String firstColumn, String secondColumn) {
            return "<tr><td><b>" + firstColumn + "</b></td><td>" + secondColumn + "</td></tr>";
        }

        private void buildSourcePanel(StyleSource s, JPanel p) {
            JosmTextArea txtSource = new JosmTextArea();
            txtSource.setFont(GuiHelper.getMonospacedFont(txtSource));
            txtSource.setEditable(false);
            p.add(new JScrollPane(txtSource), GBC.std().fill());

            try (BufferedReader reader = new BufferedReader(new InputStreamReader(s.getSourceInputStream(), StandardCharsets.UTF_8))) {
                reader.lines().forEach(line -> txtSource.append(line + '\n'));
            } catch (IOException ex) {
                Logging.error(ex);
                txtSource.append("<ERROR: failed to read file!>");
            }
            txtSource.setCaretPosition(0);
        }

        private <T> void buildErrorsOrWarningPanel(Collection<T> items, JPanel p) {
            JosmTextArea txtErrors = new JosmTextArea();
            txtErrors.setFont(GuiHelper.getMonospacedFont(txtErrors));
            txtErrors.setEditable(false);
            p.add(new JScrollPane(txtErrors), GBC.std().fill());
            for (T t : items) {
                txtErrors.append(t.toString() + '\n');
            }
            txtErrors.setCaretPosition(0);
        }
    }

    class PopupMenuHandler extends PopupMenuLauncher {
        @Override
        public void launch(MouseEvent evt) {
            if (cbWireframe.isSelected())
                return;
            super.launch(evt);
        }

        @Override
        protected void showMenu(MouseEvent evt) {
            menu = new MapPaintPopup();
            super.showMenu(evt);
        }
    }

    /**
     * The popup menu displayed when right-clicking a map paint entry
     */
    public class MapPaintPopup extends StayOpenPopupMenu {
        /**
         * Constructs a new {@code MapPaintPopup}.
         */
        public MapPaintPopup() {
            add(reloadAction);
            add(new SaveAsAction());

            JMenu setMenu = new JMenu(tr("Style settings"));
            setMenu.setIcon(new ImageProvider("dialogs/mapstyle").setMaxSize(ImageSizes.MENU).addOverlay(
                new ImageOverlay(new ImageProvider("preference"), 0.25, 0.25, 1.0, 1.0)).get());
            setMenu.setToolTipText(tr("Customize the style"));
            add(setMenu);

            final int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
            final StyleSource style = sel >= 0 && sel < model.getRowCount() ? model.getRow(sel) : null;
            if (style == null || Utils.isEmpty(style.settings)) {
                setMenu.setEnabled(false);
            } else {
                // Add settings groups
                style.settingGroups.forEach((group, settings) -> new StyleSettingGroupGui(group, settings).addMenuEntry(setMenu));
                // Add settings not in groups
                style.settings.stream()
                        .filter(s -> style.settingGroups.values().stream().flatMap(List::stream).noneMatch(s::equals))
                        .forEach(s -> s.getStyleSettingGui().addMenuEntry(setMenu));
            }

            addSeparator();
            add(new InfoAction());
        }
    }
}
