// 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.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.DefaultButtonModel;
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.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
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.Main;
import org.openstreetmap.josm.actions.ExtensionFileFilter;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.actions.PreferencesAction;
import org.openstreetmap.josm.gui.ExtendedDialog;
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.MapPaintSylesUpdateListener;
import org.openstreetmap.josm.gui.mappaint.StyleSetting;
import org.openstreetmap.josm.gui.mappaint.StyleSource;
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
import org.openstreetmap.josm.gui.preferences.SourceEntry;
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.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.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.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.forPreferenceSubTab(
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("Toggle: {0}", tr("MapPaint")),
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"), JLabel.HORIZONTAL);
wfLabel.setFont(wfLabel.getFont().deriveFont(Font.PLAIN));
wfLabel.setLabelFor(cbWireframe);
cbWireframe.setModel(new DefaultButtonModel() {
@Override
public void setSelected(boolean b) {
super.setSelected(b);
tblStyles.setEnabled(!b);
onoffAction.updateEnabledState();
upAction.updateEnabledState();
downAction.updateEnabledState();
}
});
cbWireframe.addActionListener(e -> Main.main.menu.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.addMapPaintSylesUpdateListener(model);
Main.main.menu.wireFrameToggleAction.addButtonModel(cbWireframe.getModel());
}
@Override
public void hideNotify() {
Main.main.menu.wireFrameToggleAction.removeButtonModel(cbWireframe.getModel());
MapPaintStyles.removeMapPaintSylesUpdateListener(model);
}
protected class StylesModel extends AbstractTableModel implements MapPaintSylesUpdateListener {
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 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 AbstractAction implements ListSelectionListener {
/**
* Constructs a new {@code OnOffAction}.
*/
public OnOffAction() {
putValue(NAME, tr("On/Off"));
putValue(SHORT_DESCRIPTION, tr("Turn selected styles on or off"));
new ImageProvider("apply").getResource().attachImageIcon(this, true);
updateEnabledState();
}
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);
selectionModel.setValueIsAdjusting(true);
selectionModel.clearSelection();
for (int p: pos) {
selectionModel.addSelectionInterval(p, p);
}
selectionModel.setValueIsAdjusting(false);
}
}
/**
* The action to move down the currently selected entries in the list.
*/
protected class MoveUpDownAction extends AbstractAction 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) {
increment = isDown ? 1 : -1;
putValue(NAME, isDown ? tr("Down") : tr("Up"));
new ImageProvider("dialogs", isDown ? "down" : "up").getResource().attachImageIcon(this, true);
putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
updateEnabledState();
}
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);
selectionModel.setValueIsAdjusting(true);
selectionModel.clearSelection();
for (int row: sel) {
selectionModel.addSelectionInterval(row + increment, row + increment);
}
selectionModel.setValueIsAdjusting(false);
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();
if (pos.length == 0)
return false;
for (int i : pos) {
if (!model.getRow(i).isLocal())
return false;
}
return true;
}
@Override
public void valueChanged(ListSelectionEvent e) {
setEnabled(getEnabledState());
}
@Override
public void actionPerformed(ActionEvent e) {
final int[] rows = tblStyles.getSelectedRows();
MapPaintStyles.reloadStyles(rows);
Main.worker.submit(() -> SwingUtilities.invokeLater(() -> {
selectionModel.setValueIsAdjusting(true);
selectionModel.clearSelection();
for (int r: rows) {
selectionModel.addSelectionInterval(r, r);
}
selectionModel.setValueIsAdjusting(false);
}));
}
}
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", System.getProperty("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;
Main.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) {
Main.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(Main.parent, tr("Map Style info"), new String[] {tr("Close")});
info.setPreferredSize(new Dimension(600, 400));
info.setButtonIcons(new String[] {"ok.png"});
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, JLabel.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:"), Utils.toString(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(GBC.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 {
InputStream is = s.getSourceInputStream();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
txtSource.append(line + '\n');
}
} finally {
s.closeSourceInputStream(is);
}
} catch (IOException ex) {
Main.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 JPopupMenu {
/**
* Constructs a new {@code MapPaintPopup}.
*/
public MapPaintPopup() {
add(reloadAction);
add(new SaveAsAction());
JMenu setMenu = new JMenu(tr("Style settings"));
setMenu.setIcon(new ImageProvider("preference").setMaxSize(ImageSizes.POPUPMENU).addOverlay(
new ImageOverlay(new ImageProvider("dialogs/mappaint", "pencil"), 0.5, 0.5, 1.0, 1.0)).get());
setMenu.setToolTipText(tr("Customize the style"));
add(setMenu);
int sel = tblStyles.getSelectionModel().getLeadSelectionIndex();
StyleSource style = null;
if (sel >= 0 && sel < model.getRowCount()) {
style = model.getRow(sel);
}
if (style == null || style.settings.isEmpty()) {
setMenu.setEnabled(false);
} else {
for (StyleSetting s : style.settings) {
s.addMenuEntry(setMenu);
}
}
addSeparator();
add(new InfoAction());
}
}
}