source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/SourceEditor.java@ 12906

Last change on this file since 12906 was 12846, checked in by bastiK, 7 years ago

see #15229 - use Config.getPref() wherever possible

  • Property svn:eol-style set to native
File size: 62.1 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[1743]2package org.openstreetmap.josm.gui.preferences;
3
[2400]4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
[1743]5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Component;
[3796]8import java.awt.Dimension;
9import java.awt.Font;
[2400]10import java.awt.GridBagConstraints;
[1743]11import java.awt.GridBagLayout;
[3796]12import java.awt.Insets;
[3855]13import java.awt.Rectangle;
[1743]14import java.awt.event.ActionEvent;
[2400]15import java.awt.event.FocusAdapter;
16import java.awt.event.FocusEvent;
17import java.awt.event.KeyEvent;
[3796]18import java.awt.event.MouseAdapter;
[2400]19import java.awt.event.MouseEvent;
[1743]20import java.io.BufferedReader;
[2400]21import java.io.File;
22import java.io.IOException;
23import java.net.MalformedURLException;
24import java.net.URL;
[1743]25import java.util.ArrayList;
[5572]26import java.util.Arrays;
[1743]27import java.util.Collection;
[2400]28import java.util.Collections;
29import java.util.EventObject;
[5220]30import java.util.HashMap;
[2400]31import java.util.Iterator;
32import java.util.List;
[5220]33import java.util.Map;
[7083]34import java.util.Objects;
[2400]35import java.util.concurrent.CopyOnWriteArrayList;
[1743]36import java.util.regex.Matcher;
37import java.util.regex.Pattern;
38
[2400]39import javax.swing.AbstractAction;
[1743]40import javax.swing.BorderFactory;
[3796]41import javax.swing.Box;
[1743]42import javax.swing.DefaultListModel;
[2400]43import javax.swing.DefaultListSelectionModel;
[8507]44import javax.swing.ImageIcon;
[1743]45import javax.swing.JButton;
[3855]46import javax.swing.JCheckBox;
[2400]47import javax.swing.JComponent;
48import javax.swing.JFileChooser;
[1743]49import javax.swing.JLabel;
50import javax.swing.JList;
51import javax.swing.JOptionPane;
[2017]52import javax.swing.JPanel;
[1743]53import javax.swing.JScrollPane;
[3646]54import javax.swing.JSeparator;
[2400]55import javax.swing.JTable;
[3646]56import javax.swing.JToolBar;
[2400]57import javax.swing.KeyStroke;
[1743]58import javax.swing.ListCellRenderer;
[2400]59import javax.swing.ListSelectionModel;
60import javax.swing.event.CellEditorListener;
61import javax.swing.event.ChangeEvent;
[6729]62import javax.swing.event.DocumentEvent;
63import javax.swing.event.DocumentListener;
[2400]64import javax.swing.event.ListSelectionEvent;
65import javax.swing.event.ListSelectionListener;
[3796]66import javax.swing.event.TableModelEvent;
67import javax.swing.event.TableModelListener;
[5572]68import javax.swing.filechooser.FileFilter;
[2400]69import javax.swing.table.AbstractTableModel;
[3796]70import javax.swing.table.DefaultTableCellRenderer;
[2400]71import javax.swing.table.TableCellEditor;
[11366]72import javax.swing.table.TableModel;
[1743]73
74import org.openstreetmap.josm.Main;
[5572]75import org.openstreetmap.josm.actions.ExtensionFileFilter;
[5586]76import org.openstreetmap.josm.data.Version;
[12649]77import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
78import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
79import org.openstreetmap.josm.data.preferences.sources.SourcePrefHelper;
80import org.openstreetmap.josm.data.preferences.sources.SourceProvider;
81import org.openstreetmap.josm.data.preferences.sources.SourceType;
[3796]82import org.openstreetmap.josm.gui.ExtendedDialog;
[2400]83import org.openstreetmap.josm.gui.HelpAwareOptionPane;
[12634]84import org.openstreetmap.josm.gui.MainApplication;
[2400]85import org.openstreetmap.josm.gui.PleaseWaitRunnable;
[5572]86import org.openstreetmap.josm.gui.util.FileFilterAllFiles;
[7434]87import org.openstreetmap.josm.gui.util.GuiHelper;
[5784]88import org.openstreetmap.josm.gui.util.TableHelper;
[7578]89import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
90import org.openstreetmap.josm.gui.widgets.FileChooserManager;
[5886]91import org.openstreetmap.josm.gui.widgets.JosmTextField;
[7248]92import org.openstreetmap.josm.io.CachedFile;
[7434]93import org.openstreetmap.josm.io.OnlineResource;
[2400]94import org.openstreetmap.josm.io.OsmTransferException;
[12846]95import org.openstreetmap.josm.spi.preferences.Config;
[1743]96import org.openstreetmap.josm.tools.GBC;
[8324]97import org.openstreetmap.josm.tools.ImageOverlay;
[2400]98import org.openstreetmap.josm.tools.ImageProvider;
[8324]99import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
[1755]100import org.openstreetmap.josm.tools.LanguageInfo;
[12620]101import org.openstreetmap.josm.tools.Logging;
[5586]102import org.openstreetmap.josm.tools.Utils;
[2400]103import org.xml.sax.SAXException;
[1743]104
[11367]105/**
106 * Editor for JOSM extensions source entries.
107 * @since 1743
108 */
[3797]109public abstract class SourceEditor extends JPanel {
[1743]110
[11387]111 /** the type of source entry **/
[6670]112 protected final SourceType sourceType;
[11387]113 /** determines if the entry type can be enabled (set as active) **/
[6670]114 protected final boolean canEnable;
[4072]115
[11387]116 /** the table of active sources **/
[4839]117 protected final JTable tblActiveSources;
[11387]118 /** the underlying model of active sources **/
[4839]119 protected final ActiveSourcesModel activeSourcesModel;
[11387]120 /** the list of available sources **/
[7001]121 protected final JList<ExtendedSourceEntry> lstAvailableSources;
[11387]122 /** the underlying model of available sources **/
[4839]123 protected final AvailableSourcesListModel availableSourcesModel;
[11387]124 /** the URL from which the available sources are fetched **/
[4839]125 protected final String availableSourcesUrl;
[11387]126 /** the list of source providers **/
[8308]127 protected final transient List<SourceProvider> sourceProviders;
[4839]128
[10114]129 private JTable tblIconPaths;
130 private IconPathTableModel iconPathsModel;
[6670]131
[11387]132 /** determines if the source providers have been initially loaded **/
[3797]133 protected boolean sourcesInitiallyLoaded;
[3796]134
[2400]135 /**
[6670]136 * Constructs a new {@code SourceEditor}.
137 * @param sourceType the type of source managed by this editor
[3797]138 * @param availableSourcesUrl the URL to the list of available sources
[4839]139 * @param sourceProviders the list of additional source providers, from plugins
[6670]140 * @param handleIcons {@code true} if icons may be managed, {@code false} otherwise
[2400]141 */
[6670]142 public SourceEditor(SourceType sourceType, String availableSourcesUrl, List<SourceProvider> sourceProviders, boolean handleIcons) {
[2400]143
[6670]144 this.sourceType = sourceType;
145 this.canEnable = sourceType.equals(SourceType.MAP_PAINT_STYLE) || sourceType.equals(SourceType.TAGCHECKER_RULE);
[7082]146
[2400]147 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
[6670]148 this.availableSourcesModel = new AvailableSourcesListModel(selectionModel);
[7005]149 this.lstAvailableSources = new JList<>(availableSourcesModel);
[4839]150 this.lstAvailableSources.setSelectionModel(selectionModel);
[8507]151 final SourceEntryListCellRenderer listCellRenderer = new SourceEntryListCellRenderer();
152 this.lstAvailableSources.setCellRenderer(listCellRenderer);
[10024]153 GuiHelper.extendTooltipDelay(lstAvailableSources);
[3797]154 this.availableSourcesUrl = availableSourcesUrl;
[4839]155 this.sourceProviders = sourceProviders;
[3796]156
157 selectionModel = new DefaultListSelectionModel();
[6670]158 activeSourcesModel = new ActiveSourcesModel(selectionModel);
[11366]159 tblActiveSources = new ScrollHackTable(activeSourcesModel);
[8846]160 tblActiveSources.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
[3797]161 tblActiveSources.setSelectionModel(selectionModel);
162 tblActiveSources.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
163 tblActiveSources.setShowGrid(false);
164 tblActiveSources.setIntercellSpacing(new Dimension(0, 0));
165 tblActiveSources.setTableHeader(null);
166 tblActiveSources.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
167 SourceEntryTableCellRenderer sourceEntryRenderer = new SourceEntryTableCellRenderer();
[6670]168 if (canEnable) {
[3855]169 tblActiveSources.getColumnModel().getColumn(0).setMaxWidth(1);
170 tblActiveSources.getColumnModel().getColumn(0).setResizable(false);
171 tblActiveSources.getColumnModel().getColumn(1).setCellRenderer(sourceEntryRenderer);
172 } else {
173 tblActiveSources.getColumnModel().getColumn(0).setCellRenderer(sourceEntryRenderer);
174 }
175
[10611]176 activeSourcesModel.addTableModelListener(e -> {
177 listCellRenderer.updateSources(activeSourcesModel.getSources());
178 lstAvailableSources.repaint();
[8507]179 });
[10611]180 tblActiveSources.addPropertyChangeListener(evt -> {
181 listCellRenderer.updateSources(activeSourcesModel.getSources());
182 lstAvailableSources.repaint();
[8507]183 });
[10611]184 // Force Swing to show horizontal scrollbars for the JTable
185 // Yes, this is a little ugly, but should work
186 activeSourcesModel.addTableModelListener(e -> TableHelper.adjustColumnWidth(tblActiveSources, canEnable ? 1 : 0, 800));
[3797]187 activeSourcesModel.setActiveSources(getInitialSourcesList());
[4072]188
[3797]189 final EditActiveSourceAction editActiveSourceAction = new EditActiveSourceAction();
190 tblActiveSources.getSelectionModel().addListSelectionListener(editActiveSourceAction);
191 tblActiveSources.addMouseListener(new MouseAdapter() {
[3796]192 @Override
193 public void mouseClicked(MouseEvent e) {
194 if (e.getClickCount() == 2) {
[3797]195 int row = tblActiveSources.rowAtPoint(e.getPoint());
[3855]196 int col = tblActiveSources.columnAtPoint(e.getPoint());
[3797]197 if (row < 0 || row >= tblActiveSources.getRowCount())
[3796]198 return;
[6670]199 if (canEnable && col != 1)
[3855]200 return;
[3797]201 editActiveSourceAction.actionPerformed(null);
[3796]202 }
203 }
204 });
[2400]205
[3797]206 RemoveActiveSourcesAction removeActiveSourcesAction = new RemoveActiveSourcesAction();
207 tblActiveSources.getSelectionModel().addListSelectionListener(removeActiveSourcesAction);
[8510]208 tblActiveSources.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
[3797]209 tblActiveSources.getActionMap().put("delete", removeActiveSourcesAction);
[3646]210
[3855]211 MoveUpDownAction moveUp = null;
212 MoveUpDownAction moveDown = null;
[6670]213 if (sourceType.equals(SourceType.MAP_PAINT_STYLE)) {
[3855]214 moveUp = new MoveUpDownAction(false);
215 moveDown = new MoveUpDownAction(true);
216 tblActiveSources.getSelectionModel().addListSelectionListener(moveUp);
217 tblActiveSources.getSelectionModel().addListSelectionListener(moveDown);
218 activeSourcesModel.addTableModelListener(moveUp);
219 activeSourcesModel.addTableModelListener(moveDown);
220 }
221
[3797]222 ActivateSourcesAction activateSourcesAction = new ActivateSourcesAction();
223 lstAvailableSources.addListSelectionListener(activateSourcesAction);
224 JButton activate = new JButton(activateSourcesAction);
[3646]225
226 setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
227 setLayout(new GridBagLayout());
[3796]228
229 GridBagConstraints gbc = new GridBagConstraints();
230 gbc.gridx = 0;
231 gbc.gridy = 0;
232 gbc.weightx = 0.5;
233 gbc.gridwidth = 2;
234 gbc.anchor = GBC.WEST;
235 gbc.insets = new Insets(5, 11, 0, 0);
236
237 add(new JLabel(getStr(I18nString.AVAILABLE_SOURCES)), gbc);
238
239 gbc.gridx = 2;
240 gbc.insets = new Insets(5, 0, 0, 6);
241
242 add(new JLabel(getStr(I18nString.ACTIVE_SOURCES)), gbc);
243
244 gbc.gridwidth = 1;
245 gbc.gridx = 0;
246 gbc.gridy++;
247 gbc.weighty = 0.8;
248 gbc.fill = GBC.BOTH;
249 gbc.anchor = GBC.CENTER;
250 gbc.insets = new Insets(0, 11, 0, 0);
251
[3797]252 JScrollPane sp1 = new JScrollPane(lstAvailableSources);
[3796]253 add(sp1, gbc);
254
255 gbc.gridx = 1;
256 gbc.weightx = 0.0;
257 gbc.fill = GBC.VERTICAL;
258 gbc.insets = new Insets(0, 0, 0, 0);
259
260 JToolBar middleTB = new JToolBar();
261 middleTB.setFloatable(false);
262 middleTB.setBorderPainted(false);
263 middleTB.setOpaque(false);
264 middleTB.add(Box.createHorizontalGlue());
265 middleTB.add(activate);
266 middleTB.add(Box.createHorizontalGlue());
267 add(middleTB, gbc);
268
269 gbc.gridx++;
270 gbc.weightx = 0.5;
271 gbc.fill = GBC.BOTH;
272
[3797]273 JScrollPane sp = new JScrollPane(tblActiveSources);
[3796]274 add(sp, gbc);
[3646]275 sp.setColumnHeaderView(null);
276
[3796]277 gbc.gridx++;
278 gbc.weightx = 0.0;
279 gbc.fill = GBC.VERTICAL;
280 gbc.insets = new Insets(0, 0, 0, 6);
281
[3646]282 JToolBar sideButtonTB = new JToolBar(JToolBar.VERTICAL);
283 sideButtonTB.setFloatable(false);
284 sideButtonTB.setBorderPainted(false);
285 sideButtonTB.setOpaque(false);
[3797]286 sideButtonTB.add(new NewActiveSourceAction());
287 sideButtonTB.add(editActiveSourceAction);
288 sideButtonTB.add(removeActiveSourcesAction);
[3855]289 sideButtonTB.addSeparator(new Dimension(12, 30));
[6670]290 if (sourceType.equals(SourceType.MAP_PAINT_STYLE)) {
[3855]291 sideButtonTB.add(moveUp);
292 sideButtonTB.add(moveDown);
293 }
[3796]294 add(sideButtonTB, gbc);
[3646]295
[3796]296 gbc.gridx = 0;
297 gbc.gridy++;
298 gbc.weighty = 0.0;
299 gbc.weightx = 0.5;
300 gbc.fill = GBC.HORIZONTAL;
301 gbc.anchor = GBC.WEST;
302 gbc.insets = new Insets(0, 11, 0, 0);
[3646]303
[3816]304 JToolBar bottomLeftTB = new JToolBar();
[3796]305 bottomLeftTB.setFloatable(false);
306 bottomLeftTB.setBorderPainted(false);
307 bottomLeftTB.setOpaque(false);
[4839]308 bottomLeftTB.add(new ReloadSourcesAction(availableSourcesUrl, sourceProviders));
[3816]309 bottomLeftTB.add(Box.createHorizontalGlue());
[3796]310 add(bottomLeftTB, gbc);
[3646]311
[3796]312 gbc.gridx = 2;
313 gbc.anchor = GBC.CENTER;
314 gbc.insets = new Insets(0, 0, 0, 0);
[2400]315
[3796]316 JToolBar bottomRightTB = new JToolBar();
317 bottomRightTB.setFloatable(false);
318 bottomRightTB.setBorderPainted(false);
319 bottomRightTB.setOpaque(false);
320 bottomRightTB.add(Box.createHorizontalGlue());
321 bottomRightTB.add(new JButton(new ResetAction()));
322 add(bottomRightTB, gbc);
[2400]323
[8510]324 // Icon configuration
[6670]325 if (handleIcons) {
326 buildIcons(gbc);
327 }
328 }
329
330 private void buildIcons(GridBagConstraints gbc) {
331 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
332 iconPathsModel = new IconPathTableModel(selectionModel);
333 tblIconPaths = new JTable(iconPathsModel);
[3796]334 tblIconPaths.setSelectionModel(selectionModel);
335 tblIconPaths.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
336 tblIconPaths.setTableHeader(null);
337 tblIconPaths.getColumnModel().getColumn(0).setCellEditor(new FileOrUrlCellEditor(false));
338 tblIconPaths.setRowHeight(20);
[8377]339 tblIconPaths.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
[3796]340 iconPathsModel.setIconPaths(getInitialIconPathsList());
[2400]341
[3796]342 EditIconPathAction editIconPathAction = new EditIconPathAction();
343 tblIconPaths.getSelectionModel().addListSelectionListener(editIconPathAction);
[3321]344
[3796]345 RemoveIconPathAction removeIconPathAction = new RemoveIconPathAction();
346 tblIconPaths.getSelectionModel().addListSelectionListener(removeIconPathAction);
[8510]347 tblIconPaths.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
[3796]348 tblIconPaths.getActionMap().put("delete", removeIconPathAction);
[3321]349
[3796]350 gbc.gridx = 0;
351 gbc.gridy++;
352 gbc.weightx = 1.0;
353 gbc.gridwidth = GBC.REMAINDER;
354 gbc.insets = new Insets(8, 11, 8, 6);
355
356 add(new JSeparator(), gbc);
357
358 gbc.gridy++;
359 gbc.insets = new Insets(0, 11, 0, 6);
360
361 add(new JLabel(tr("Icon paths:")), gbc);
362
363 gbc.gridy++;
364 gbc.weighty = 0.2;
365 gbc.gridwidth = 3;
366 gbc.fill = GBC.BOTH;
367 gbc.insets = new Insets(0, 11, 0, 0);
368
[6670]369 JScrollPane sp = new JScrollPane(tblIconPaths);
370 add(sp, gbc);
[3796]371 sp.setColumnHeaderView(null);
372
373 gbc.gridx = 3;
374 gbc.gridwidth = 1;
375 gbc.weightx = 0.0;
376 gbc.fill = GBC.VERTICAL;
377 gbc.insets = new Insets(0, 0, 0, 6);
378
379 JToolBar sideButtonTBIcons = new JToolBar(JToolBar.VERTICAL);
380 sideButtonTBIcons.setFloatable(false);
381 sideButtonTBIcons.setBorderPainted(false);
382 sideButtonTBIcons.setOpaque(false);
383 sideButtonTBIcons.add(new NewIconPathAction());
384 sideButtonTBIcons.add(editIconPathAction);
385 sideButtonTBIcons.add(removeIconPathAction);
386 add(sideButtonTBIcons, gbc);
[3321]387 }
388
[3796]389 /**
390 * Load the list of source entries that the user has configured.
[8958]391 * @return list of source entries that the user has configured
[3796]392 */
[6883]393 public abstract Collection<? extends SourceEntry> getInitialSourcesList();
[2400]394
[3796]395 /**
396 * Load the list of configured icon paths.
[8958]397 * @return list of configured icon paths
[3796]398 */
[6883]399 public abstract Collection<String> getInitialIconPathsList();
[2400]400
[3796]401 /**
402 * Get the default list of entries (used when resetting the list).
[8958]403 * @return default list of entries
[3796]404 */
[6883]405 public abstract Collection<ExtendedSourceEntry> getDefault();
[2400]406
[3796]407 /**
408 * Save the settings after user clicked "Ok".
409 * @return true if restart is required
410 */
[6883]411 public abstract boolean finish();
[3796]412
[11387]413 /**
414 * Default implementation of {@link #finish}.
415 * @param prefHelper Helper class for specialized extensions preferences
416 * @param iconPref icons path preference
417 * @return true if restart is required
418 */
[10114]419 protected boolean doFinish(SourcePrefHelper prefHelper, String iconPref) {
420 boolean changed = prefHelper.put(activeSourcesModel.getSources());
421
422 if (tblIconPaths != null) {
423 List<String> iconPaths = iconPathsModel.getIconPaths();
424
425 if (!iconPaths.isEmpty()) {
[12846]426 if (Config.getPref().putList(iconPref, iconPaths)) {
[10114]427 changed = true;
428 }
[12846]429 } else if (Config.getPref().putList(iconPref, null)) {
[10114]430 changed = true;
431 }
432 }
433 return changed;
434 }
435
[3796]436 /**
[9239]437 * Provide the GUI strings. (There are differences for MapPaint, Preset and TagChecker Rule)
438 * @param ident any {@link I18nString} value
439 * @return the translated string for {@code ident}
[3796]440 */
[6883]441 protected abstract String getStr(I18nString ident);
[3796]442
[11366]443 static final class ScrollHackTable extends JTable {
444 ScrollHackTable(TableModel dm) {
445 super(dm);
446 }
447
448 // some kind of hack to prevent the table from scrolling slightly to the right when clicking on the text
449 @Override
450 public void scrollRectToVisible(Rectangle aRect) {
451 super.scrollRectToVisible(new Rectangle(0, aRect.y, aRect.width, aRect.height));
452 }
453 }
454
[3796]455 /**
456 * Identifiers for strings that need to be provided.
457 */
[9239]458 public enum I18nString {
459 /** Available (styles|presets|rules) */
460 AVAILABLE_SOURCES,
461 /** Active (styles|presets|rules) */
462 ACTIVE_SOURCES,
463 /** Add a new (style|preset|rule) by entering filename or URL */
464 NEW_SOURCE_ENTRY_TOOLTIP,
465 /** New (style|preset|rule) entry */
466 NEW_SOURCE_ENTRY,
467 /** Remove the selected (styles|presets|rules) from the list of active (styles|presets|rules) */
468 REMOVE_SOURCE_TOOLTIP,
469 /** Edit the filename or URL for the selected active (style|preset|rule) */
470 EDIT_SOURCE_TOOLTIP,
471 /** Add the selected available (styles|presets|rules) to the list of active (styles|presets|rules) */
472 ACTIVATE_TOOLTIP,
473 /** Reloads the list of available (styles|presets|rules) */
474 RELOAD_ALL_AVAILABLE,
475 /** Loading (style|preset|rule) sources */
476 LOADING_SOURCES_FROM,
477 /** Failed to load the list of (style|preset|rule) sources */
478 FAILED_TO_LOAD_SOURCES_FROM,
479 /** /Preferences/(Styles|Presets|Rules)#FailedToLoad(Style|Preset|Rule)Sources */
480 FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC,
481 /** Illegal format of entry in (style|preset|rule) list */
482 ILLEGAL_FORMAT_OF_ENTRY
483 }
[3796]484
[9417]485 /**
486 * Determines whether the list of active sources has changed.
487 * @return {@code true} if the list of active sources has changed, {@code false} otherwise
488 */
[3797]489 public boolean hasActiveSourcesChanged() {
[3796]490 Collection<? extends SourceEntry> prev = getInitialSourcesList();
[3797]491 List<SourceEntry> cur = activeSourcesModel.getSources();
[3796]492 if (prev.size() != cur.size())
493 return true;
494 Iterator<? extends SourceEntry> p = prev.iterator();
495 Iterator<SourceEntry> c = cur.iterator();
496 while (p.hasNext()) {
497 SourceEntry pe = p.next();
498 SourceEntry ce = c.next();
[7083]499 if (!Objects.equals(pe.url, ce.url) || !Objects.equals(pe.name, ce.name) || pe.active != ce.active)
[3796]500 return true;
[2400]501 }
[3796]502 return false;
[2400]503 }
504
[9417]505 /**
506 * Returns the list of active sources.
507 * @return the list of active sources
508 */
[3797]509 public Collection<SourceEntry> getActiveSources() {
510 return activeSourcesModel.getSources();
[3796]511 }
512
[8681]513 /**
514 * Synchronously loads available sources and returns the parsed list.
[8936]515 * @return list of available sources
[10235]516 * @throws OsmTransferException in case of OSM transfer error
517 * @throws IOException in case of any I/O error
518 * @throws SAXException in case of any SAX error
[8681]519 */
[10235]520 public final Collection<ExtendedSourceEntry> loadAndGetAvailableSources() throws SAXException, IOException, OsmTransferException {
521 final SourceLoader loader = new SourceLoader(availableSourcesUrl, sourceProviders);
522 loader.realRun();
523 return loader.sources;
[8681]524 }
525
[9417]526 /**
527 * Remove sources associated with given indexes from active list.
528 * @param idxs indexes of sources to remove
529 */
[3796]530 public void removeSources(Collection<Integer> idxs) {
[3797]531 activeSourcesModel.removeIdxs(idxs);
[3796]532 }
533
[11387]534 /**
535 * Reload available sources.
536 * @param url the URL from which the available sources are fetched
537 * @param sourceProviders the list of source providers
538 */
[4839]539 protected void reloadAvailableSources(String url, List<SourceProvider> sourceProviders) {
[12634]540 MainApplication.worker.submit(new SourceLoader(url, sourceProviders));
[2400]541 }
542
[8936]543 /**
544 * Performs the initial loading of source providers. Does nothing if already done.
545 */
[3797]546 public void initiallyLoadAvailableSources() {
547 if (!sourcesInitiallyLoaded) {
[4839]548 reloadAvailableSources(availableSourcesUrl, sourceProviders);
[2400]549 }
[3797]550 sourcesInitiallyLoaded = true;
[2400]551 }
552
[11387]553 /**
554 * List model of available sources.
555 */
[7001]556 protected static class AvailableSourcesListModel extends DefaultListModel<ExtendedSourceEntry> {
[9078]557 private final transient List<ExtendedSourceEntry> data;
558 private final DefaultListSelectionModel selectionModel;
[2400]559
[11387]560 /**
561 * Constructs a new {@code AvailableSourcesListModel}
562 * @param selectionModel selection model
563 */
[3797]564 public AvailableSourcesListModel(DefaultListSelectionModel selectionModel) {
[7005]565 data = new ArrayList<>();
[2400]566 this.selectionModel = selectionModel;
567 }
568
[11387]569 /**
570 * Sets the source list.
571 * @param sources source list
572 */
[3797]573 public void setSources(List<ExtendedSourceEntry> sources) {
[2400]574 data.clear();
[3797]575 if (sources != null) {
576 data.addAll(sources);
[2400]577 }
578 fireContentsChanged(this, 0, data.size());
579 }
580
581 @Override
[7001]582 public ExtendedSourceEntry getElementAt(int index) {
[2400]583 return data.get(index);
584 }
585
586 @Override
587 public int getSize() {
588 if (data == null) return 0;
589 return data.size();
590 }
591
[11387]592 /**
593 * Deletes the selected sources.
594 */
[2400]595 public void deleteSelected() {
[3797]596 Iterator<ExtendedSourceEntry> it = data.iterator();
[8510]597 int i = 0;
598 while (it.hasNext()) {
[2400]599 it.next();
600 if (selectionModel.isSelectedIndex(i)) {
601 it.remove();
602 }
603 i++;
604 }
605 fireContentsChanged(this, 0, data.size());
606 }
607
[11387]608 /**
609 * Returns the selected sources.
610 * @return the selected sources
611 */
[3797]612 public List<ExtendedSourceEntry> getSelected() {
[7005]613 List<ExtendedSourceEntry> ret = new ArrayList<>();
[8510]614 for (int i = 0; i < data.size(); i++) {
[2400]615 if (selectionModel.isSelectedIndex(i)) {
616 ret.add(data.get(i));
617 }
618 }
619 return ret;
620 }
621 }
622
[11387]623 /**
624 * Table model of active sources.
625 */
[3855]626 protected class ActiveSourcesModel extends AbstractTableModel {
[8308]627 private transient List<SourceEntry> data;
[9078]628 private final DefaultListSelectionModel selectionModel;
[2400]629
[11387]630 /**
631 * Constructs a new {@code ActiveSourcesModel}.
632 * @param selectionModel selection model
633 */
[3797]634 public ActiveSourcesModel(DefaultListSelectionModel selectionModel) {
[2400]635 this.selectionModel = selectionModel;
[7005]636 this.data = new ArrayList<>();
[2400]637 }
638
[6084]639 @Override
[2400]640 public int getColumnCount() {
[6670]641 return canEnable ? 2 : 1;
[2400]642 }
643
[6084]644 @Override
[2400]645 public int getRowCount() {
646 return data == null ? 0 : data.size();
647 }
648
[3796]649 @Override
[3855]650 public Object getValueAt(int rowIndex, int columnIndex) {
[6670]651 if (canEnable && columnIndex == 0)
[3855]652 return data.get(rowIndex).active;
653 else
654 return data.get(rowIndex);
[2400]655 }
656
657 @Override
658 public boolean isCellEditable(int rowIndex, int columnIndex) {
[6670]659 return canEnable && columnIndex == 0;
[2400]660 }
661
662 @Override
[3855]663 public Class<?> getColumnClass(int column) {
[6670]664 if (canEnable && column == 0)
[5105]665 return Boolean.class;
666 else return SourceEntry.class;
[2400]667 }
668
[3855]669 @Override
670 public void setValueAt(Object aValue, int row, int column) {
671 if (row < 0 || row >= getRowCount() || aValue == null)
672 return;
[6670]673 if (canEnable && column == 0) {
[8444]674 data.get(row).active = !data.get(row).active;
[3855]675 }
676 }
677
[11387]678 /**
679 * Sets active sources.
680 * @param sources active sources
681 */
[3797]682 public void setActiveSources(Collection<? extends SourceEntry> sources) {
[2400]683 data.clear();
[3796]684 if (sources != null) {
[3855]685 for (SourceEntry e : sources) {
686 data.add(new SourceEntry(e));
687 }
[2400]688 }
689 fireTableDataChanged();
690 }
691
[11387]692 /**
693 * Adds an active source.
694 * @param entry source to add
695 */
[3797]696 public void addSource(SourceEntry entry) {
697 if (entry == null) return;
698 data.add(entry);
[2400]699 fireTableDataChanged();
[3797]700 int idx = data.indexOf(entry);
[2400]701 if (idx >= 0) {
702 selectionModel.setSelectionInterval(idx, idx);
703 }
704 }
705
[11387]706 /**
707 * Removes the selected sources.
708 */
[2400]709 public void removeSelected() {
[3796]710 Iterator<SourceEntry> it = data.iterator();
[8510]711 int i = 0;
712 while (it.hasNext()) {
[2400]713 it.next();
714 if (selectionModel.isSelectedIndex(i)) {
715 it.remove();
716 }
717 i++;
718 }
719 fireTableDataChanged();
720 }
721
[11387]722 /**
723 * Removes the sources at given indexes.
724 * @param idxs indexes to remove
725 */
[3796]726 public void removeIdxs(Collection<Integer> idxs) {
[7005]727 List<SourceEntry> newData = new ArrayList<>();
[8510]728 for (int i = 0; i < data.size(); ++i) {
[3796]729 if (!idxs.contains(i)) {
730 newData.add(data.get(i));
731 }
732 }
733 data = newData;
[3321]734 fireTableDataChanged();
735 }
736
[11387]737 /**
738 * Adds multiple sources.
739 * @param sources source entries
740 */
[3797]741 public void addExtendedSourceEntries(List<ExtendedSourceEntry> sources) {
[2400]742 if (sources == null) return;
[3797]743 for (ExtendedSourceEntry info: sources) {
[12825]744 data.add(new SourceEntry(info.type, info.url, info.name, info.getDisplayName(), true));
[2400]745 }
746 fireTableDataChanged();
[11365]747 selectionModel.setValueIsAdjusting(true);
[2400]748 selectionModel.clearSelection();
[3797]749 for (ExtendedSourceEntry info: sources) {
[3796]750 int pos = data.indexOf(info);
[8510]751 if (pos >= 0) {
[2400]752 selectionModel.addSelectionInterval(pos, pos);
753 }
754 }
[11365]755 selectionModel.setValueIsAdjusting(false);
[2400]756 }
757
[11387]758 /**
759 * Returns the active sources.
760 * @return the active sources
761 */
[3797]762 public List<SourceEntry> getSources() {
[7005]763 return new ArrayList<>(data);
[2400]764 }
[3855]765
766 public boolean canMove(int i) {
767 int[] sel = tblActiveSources.getSelectedRows();
768 if (sel.length == 0)
769 return false;
[4072]770 if (i < 0)
[3855]771 return sel[0] >= -i;
[4072]772 else if (i > 0)
773 return sel[sel.length-1] <= getRowCount()-1 - i;
774 else
775 return true;
[3855]776 }
777
778 public void move(int i) {
779 if (!canMove(i)) return;
780 int[] sel = tblActiveSources.getSelectedRows();
781 for (int row: sel) {
782 SourceEntry t1 = data.get(row);
783 SourceEntry t2 = data.get(row + i);
784 data.set(row, t2);
785 data.set(row + i, t1);
786 }
[11365]787 selectionModel.setValueIsAdjusting(true);
[3855]788 selectionModel.clearSelection();
789 for (int row: sel) {
790 selectionModel.addSelectionInterval(row + i, row + i);
791 }
[11365]792 selectionModel.setValueIsAdjusting(false);
[3855]793 }
[2400]794 }
795
[7578]796 private static void prepareFileChooser(String url, AbstractFileChooser fc) {
[8394]797 if (url == null || url.trim().isEmpty()) return;
[7092]798 URL sourceUrl = null;
799 try {
800 sourceUrl = new URL(url);
[8510]801 } catch (MalformedURLException e) {
[7092]802 File f = new File(url);
803 if (f.isFile()) {
804 f = f.getParentFile();
805 }
806 if (f != null) {
807 fc.setCurrentDirectory(f);
808 }
809 return;
810 }
811 if (sourceUrl.getProtocol().startsWith("file")) {
812 File f = new File(sourceUrl.getPath());
813 if (f.isFile()) {
814 f = f.getParentFile();
815 }
816 if (f != null) {
817 fc.setCurrentDirectory(f);
818 }
819 }
820 }
821
[11387]822 /**
823 * Dialog to edit a source entry.
824 */
[3796]825 protected class EditSourceEntryDialog extends ExtendedDialog {
826
[9078]827 private final JosmTextField tfTitle;
828 private final JosmTextField tfURL;
[3855]829 private JCheckBox cbActive;
[3796]830
[11387]831 /**
832 * Constructs a new {@code EditSourceEntryDialog}.
833 * @param parent parent component
834 * @param title dialog title
835 * @param e source entry to edit
836 */
[3796]837 public EditSourceEntryDialog(Component parent, String title, SourceEntry e) {
[12279]838 super(parent, title, tr("Ok"), tr("Cancel"));
[3796]839
840 JPanel p = new JPanel(new GridBagLayout());
841
[5886]842 tfTitle = new JosmTextField(60);
[3796]843 p.add(new JLabel(tr("Name (optional):")), GBC.std().insets(15, 0, 5, 5));
[3894]844 p.add(tfTitle, GBC.eol().insets(0, 0, 5, 5));
[3796]845
[5886]846 tfURL = new JosmTextField(60);
[3796]847 p.add(new JLabel(tr("URL / File:")), GBC.std().insets(15, 0, 5, 0));
[3855]848 p.add(tfURL, GBC.std().insets(0, 0, 5, 5));
[3796]849 JButton fileChooser = new JButton(new LaunchFileChooserAction());
850 fileChooser.setMargin(new Insets(0, 0, 0, 0));
[3855]851 p.add(fileChooser, GBC.eol().insets(0, 0, 5, 5));
[7082]852
[3796]853 if (e != null) {
[3894]854 if (e.title != null) {
855 tfTitle.setText(e.title);
[3796]856 }
857 tfURL.setText(e.url);
858 }
859
[6670]860 if (canEnable) {
[9074]861 cbActive = new JCheckBox(tr("active"), e == null || e.active);
[3855]862 p.add(cbActive, GBC.eol().insets(15, 0, 5, 0));
863 }
[12279]864 setButtonIcons("ok", "cancel");
[3796]865 setContent(p);
[6729]866
867 // Make OK button enabled only when a file/URL has been set
868 tfURL.getDocument().addDocumentListener(new DocumentListener() {
869 @Override
870 public void insertUpdate(DocumentEvent e) {
871 updateOkButtonState();
872 }
[8510]873
[6729]874 @Override
875 public void removeUpdate(DocumentEvent e) {
876 updateOkButtonState();
877 }
[8510]878
[6729]879 @Override
880 public void changedUpdate(DocumentEvent e) {
881 updateOkButtonState();
882 }
883 });
[3796]884 }
885
[6729]886 private void updateOkButtonState() {
[11435]887 buttons.get(0).setEnabled(!Utils.isStripEmpty(tfURL.getText()));
[6729]888 }
889
890 @Override
891 public void setupDialog() {
892 super.setupDialog();
893 updateOkButtonState();
894 }
895
[3796]896 class LaunchFileChooserAction extends AbstractAction {
[8836]897 LaunchFileChooserAction() {
[10482]898 new ImageProvider("open").getResource().attachImageIcon(this);
[3796]899 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
900 }
901
[6084]902 @Override
[3796]903 public void actionPerformed(ActionEvent e) {
[5572]904 FileFilter ff;
[6670]905 switch (sourceType) {
906 case MAP_PAINT_STYLE:
[5576]907 ff = new ExtensionFileFilter("xml,mapcss,css,zip", "xml", tr("Map paint style file (*.xml, *.mapcss, *.zip)"));
[6670]908 break;
909 case TAGGING_PRESET:
[5576]910 ff = new ExtensionFileFilter("xml,zip", "xml", tr("Preset definition file (*.xml, *.zip)"));
[6670]911 break;
912 case TAGCHECKER_RULE:
913 ff = new ExtensionFileFilter("validator.mapcss,zip", "validator.mapcss", tr("Tag checker rule (*.validator.mapcss, *.zip)"));
914 break;
915 default:
[12620]916 Logging.error("Unsupported source type: "+sourceType);
[6670]917 return;
[5572]918 }
[7578]919 FileChooserManager fcm = new FileChooserManager(true)
[5572]920 .createFileChooser(true, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY);
[5438]921 prepareFileChooser(tfURL.getText(), fcm.getFileChooser());
[10035]922 AbstractFileChooser fc = fcm.openFileChooser(GuiHelper.getFrameForComponent(SourceEditor.this));
[5438]923 if (fc != null) {
924 tfURL.setText(fc.getSelectedFile().toString());
925 }
[3796]926 }
927 }
928
[4072]929 @Override
[3894]930 public String getTitle() {
931 return tfTitle.getText();
[3796]932 }
933
[11387]934 /**
935 * Returns the entered URL / File.
936 * @return the entered URL / File
937 */
[3796]938 public String getURL() {
939 return tfURL.getText();
940 }
[3855]941
[11387]942 /**
943 * Determines if the active combobox is selected.
944 * @return {@code true} if the active combobox is selected
945 */
[3855]946 public boolean active() {
[6670]947 if (!canEnable)
[3855]948 throw new UnsupportedOperationException();
949 return cbActive.isSelected();
950 }
[3796]951 }
952
[3797]953 class NewActiveSourceAction extends AbstractAction {
[8836]954 NewActiveSourceAction() {
[2400]955 putValue(NAME, tr("New"));
[3797]956 putValue(SHORT_DESCRIPTION, getStr(I18nString.NEW_SOURCE_ENTRY_TOOLTIP));
[10482]957 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
[2400]958 }
959
[6084]960 @Override
[3796]961 public void actionPerformed(ActionEvent evt) {
962 EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
[3797]963 SourceEditor.this,
[3796]964 getStr(I18nString.NEW_SOURCE_ENTRY),
965 null);
966 editEntryDialog.showDialog();
967 if (editEntryDialog.getValue() == 1) {
[3855]968 boolean active = true;
[6670]969 if (canEnable) {
[3855]970 active = editEntryDialog.active();
971 }
[12825]972 final SourceEntry entry = new SourceEntry(sourceType,
[3796]973 editEntryDialog.getURL(),
[8835]974 null, editEntryDialog.getTitle(), active);
975 entry.title = getTitleForSourceEntry(entry);
976 activeSourcesModel.addSource(entry);
[3797]977 activeSourcesModel.fireTableDataChanged();
[3796]978 }
[2400]979 }
980 }
981
[3797]982 class RemoveActiveSourcesAction extends AbstractAction implements ListSelectionListener {
[2400]983
[8836]984 RemoveActiveSourcesAction() {
[2400]985 putValue(NAME, tr("Remove"));
[3796]986 putValue(SHORT_DESCRIPTION, getStr(I18nString.REMOVE_SOURCE_TOOLTIP));
[10482]987 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
[2400]988 updateEnabledState();
989 }
990
[6890]991 protected final void updateEnabledState() {
[3797]992 setEnabled(tblActiveSources.getSelectedRowCount() > 0);
[2400]993 }
994
[6084]995 @Override
[2400]996 public void valueChanged(ListSelectionEvent e) {
997 updateEnabledState();
998 }
999
[6084]1000 @Override
[2400]1001 public void actionPerformed(ActionEvent e) {
[3797]1002 activeSourcesModel.removeSelected();
[2400]1003 }
1004 }
1005
[3797]1006 class EditActiveSourceAction extends AbstractAction implements ListSelectionListener {
[8836]1007 EditActiveSourceAction() {
[2400]1008 putValue(NAME, tr("Edit"));
[3796]1009 putValue(SHORT_DESCRIPTION, getStr(I18nString.EDIT_SOURCE_TOOLTIP));
[10482]1010 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this);
[2400]1011 updateEnabledState();
1012 }
1013
[6890]1014 protected final void updateEnabledState() {
[3797]1015 setEnabled(tblActiveSources.getSelectedRowCount() == 1);
[2400]1016 }
1017
[6084]1018 @Override
[2400]1019 public void valueChanged(ListSelectionEvent e) {
1020 updateEnabledState();
1021 }
1022
[6084]1023 @Override
[3796]1024 public void actionPerformed(ActionEvent evt) {
[3797]1025 int pos = tblActiveSources.getSelectedRow();
1026 if (pos < 0 || pos >= tblActiveSources.getRowCount())
[3796]1027 return;
1028
[3855]1029 SourceEntry e = (SourceEntry) activeSourcesModel.getValueAt(pos, 1);
[3796]1030
1031 EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
[3797]1032 SourceEditor.this, tr("Edit source entry:"), e);
[3796]1033 editEntryDialog.showDialog();
1034 if (editEntryDialog.getValue() == 1) {
[7083]1035 if (e.title != null || !"".equals(editEntryDialog.getTitle())) {
[3894]1036 e.title = editEntryDialog.getTitle();
[8835]1037 e.title = getTitleForSourceEntry(e);
[3796]1038 }
1039 e.url = editEntryDialog.getURL();
[6670]1040 if (canEnable) {
[3855]1041 e.active = editEntryDialog.active();
1042 }
1043 activeSourcesModel.fireTableRowsUpdated(pos, pos);
[3796]1044 }
[2400]1045 }
1046 }
1047
[3855]1048 /**
1049 * The action to move the currently selected entries up or down in the list.
1050 */
1051 class MoveUpDownAction extends AbstractAction implements ListSelectionListener, TableModelListener {
[8285]1052 private final int increment;
[8510]1053
[8836]1054 MoveUpDownAction(boolean isDown) {
[3855]1055 increment = isDown ? 1 : -1;
1056 putValue(SMALL_ICON, isDown ? ImageProvider.get("dialogs", "down") : ImageProvider.get("dialogs", "up"));
1057 putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
1058 updateEnabledState();
1059 }
1060
[6890]1061 public final void updateEnabledState() {
[3855]1062 setEnabled(activeSourcesModel.canMove(increment));
1063 }
1064
1065 @Override
1066 public void actionPerformed(ActionEvent e) {
1067 activeSourcesModel.move(increment);
1068 }
1069
[6084]1070 @Override
[3855]1071 public void valueChanged(ListSelectionEvent e) {
1072 updateEnabledState();
1073 }
1074
[6084]1075 @Override
[3855]1076 public void tableChanged(TableModelEvent e) {
1077 updateEnabledState();
1078 }
1079 }
1080
[3797]1081 class ActivateSourcesAction extends AbstractAction implements ListSelectionListener {
[8836]1082 ActivateSourcesAction() {
[3796]1083 putValue(SHORT_DESCRIPTION, getStr(I18nString.ACTIVATE_TOOLTIP));
[10482]1084 new ImageProvider("preferences", "activate-right").getResource().attachImageIcon(this);
[2400]1085 updateEnabledState();
1086 }
1087
[6890]1088 protected final void updateEnabledState() {
[3797]1089 setEnabled(lstAvailableSources.getSelectedIndices().length > 0);
[2400]1090 }
1091
[6084]1092 @Override
[2400]1093 public void valueChanged(ListSelectionEvent e) {
1094 updateEnabledState();
1095 }
1096
[6084]1097 @Override
[2400]1098 public void actionPerformed(ActionEvent e) {
[3797]1099 List<ExtendedSourceEntry> sources = availableSourcesModel.getSelected();
[5586]1100 int josmVersion = Version.getInstance().getVersion();
1101 if (josmVersion != Version.JOSM_UNKNOWN_VERSION) {
[7005]1102 Collection<String> messages = new ArrayList<>();
[5586]1103 for (ExtendedSourceEntry entry : sources) {
1104 if (entry.minJosmVersion != null && entry.minJosmVersion > josmVersion) {
1105 messages.add(tr("Entry ''{0}'' requires JOSM Version {1}. (Currently running: {2})",
1106 entry.title,
1107 Integer.toString(entry.minJosmVersion),
1108 Integer.toString(josmVersion))
1109 );
1110 }
1111 }
1112 if (!messages.isEmpty()) {
[12279]1113 ExtendedDialog dlg = new ExtendedDialog(Main.parent, tr("Warning"), tr("Cancel"), tr("Continue anyway"));
1114 dlg.setButtonIcons(
[5586]1115 ImageProvider.get("cancel"),
[8324]1116 new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).addOverlay(
1117 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()
[12279]1118 );
1119 dlg.setToolTipTexts(
[5586]1120 tr("Cancel and return to the previous dialog"),
[12279]1121 tr("Ignore warning and install style anyway"));
[5586]1122 dlg.setContent("<html>" + tr("Some entries have unmet dependencies:") +
1123 "<br>" + Utils.join("<br>", messages) + "</html>");
1124 dlg.setIcon(JOptionPane.WARNING_MESSAGE);
1125 if (dlg.showDialog().getValue() != 2)
1126 return;
1127 }
1128 }
[3797]1129 activeSourcesModel.addExtendedSourceEntries(sources);
[2400]1130 }
1131 }
1132
[3796]1133 class ResetAction extends AbstractAction {
1134
[8836]1135 ResetAction() {
[3796]1136 putValue(NAME, tr("Reset"));
1137 putValue(SHORT_DESCRIPTION, tr("Reset to default"));
[10482]1138 new ImageProvider("preferences", "reset").getResource().attachImageIcon(this);
[3796]1139 }
1140
[6084]1141 @Override
[3796]1142 public void actionPerformed(ActionEvent e) {
[3797]1143 activeSourcesModel.setActiveSources(getDefault());
[3796]1144 }
1145 }
1146
[3797]1147 class ReloadSourcesAction extends AbstractAction {
[4839]1148 private final String url;
[8308]1149 private final transient List<SourceProvider> sourceProviders;
[8510]1150
[8836]1151 ReloadSourcesAction(String url, List<SourceProvider> sourceProviders) {
[2400]1152 putValue(NAME, tr("Reload"));
[3796]1153 putValue(SHORT_DESCRIPTION, tr(getStr(I18nString.RELOAD_ALL_AVAILABLE), url));
[10482]1154 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
[2400]1155 this.url = url;
[4839]1156 this.sourceProviders = sourceProviders;
[7434]1157 setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE));
[2400]1158 }
1159
[6084]1160 @Override
[2400]1161 public void actionPerformed(ActionEvent e) {
[7248]1162 CachedFile.cleanup(url);
[4839]1163 reloadAvailableSources(url, sourceProviders);
[2400]1164 }
1165 }
1166
[11387]1167 /**
1168 * Table model for icons paths.
1169 */
[3796]1170 protected static class IconPathTableModel extends AbstractTableModel {
[9078]1171 private final List<String> data;
1172 private final DefaultListSelectionModel selectionModel;
[2400]1173
[11387]1174 /**
1175 * Constructs a new {@code IconPathTableModel}.
1176 * @param selectionModel selection model
1177 */
[2400]1178 public IconPathTableModel(DefaultListSelectionModel selectionModel) {
1179 this.selectionModel = selectionModel;
[7005]1180 this.data = new ArrayList<>();
[2400]1181 }
1182
[6084]1183 @Override
[2400]1184 public int getColumnCount() {
1185 return 1;
1186 }
1187
[6084]1188 @Override
[2400]1189 public int getRowCount() {
1190 return data == null ? 0 : data.size();
1191 }
1192
[6084]1193 @Override
[2400]1194 public Object getValueAt(int rowIndex, int columnIndex) {
1195 return data.get(rowIndex);
1196 }
1197
1198 @Override
1199 public boolean isCellEditable(int rowIndex, int columnIndex) {
1200 return true;
1201 }
1202
1203 @Override
1204 public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
[8510]1205 updatePath(rowIndex, (String) aValue);
[2400]1206 }
1207
[11387]1208 /**
1209 * Sets the icons paths.
1210 * @param paths icons paths
1211 */
[3797]1212 public void setIconPaths(Collection<String> paths) {
[2400]1213 data.clear();
[8510]1214 if (paths != null) {
[3797]1215 data.addAll(paths);
[2400]1216 }
1217 sort();
1218 fireTableDataChanged();
1219 }
1220
[11387]1221 /**
1222 * Adds an icon path.
1223 * @param path icon path to add
1224 */
[2400]1225 public void addPath(String path) {
1226 if (path == null) return;
1227 data.add(path);
1228 sort();
1229 fireTableDataChanged();
1230 int idx = data.indexOf(path);
1231 if (idx >= 0) {
1232 selectionModel.setSelectionInterval(idx, idx);
1233 }
1234 }
1235
[11387]1236 /**
1237 * Updates icon path at given index.
1238 * @param pos position
1239 * @param path new path
1240 */
[2400]1241 public void updatePath(int pos, String path) {
1242 if (path == null) return;
1243 if (pos < 0 || pos >= getRowCount()) return;
1244 data.set(pos, path);
1245 sort();
1246 fireTableDataChanged();
1247 int idx = data.indexOf(path);
1248 if (idx >= 0) {
1249 selectionModel.setSelectionInterval(idx, idx);
1250 }
1251 }
1252
[11387]1253 /**
1254 * Removes the selected path.
1255 */
[2400]1256 public void removeSelected() {
1257 Iterator<String> it = data.iterator();
[8510]1258 int i = 0;
1259 while (it.hasNext()) {
[2400]1260 it.next();
1261 if (selectionModel.isSelectedIndex(i)) {
1262 it.remove();
1263 }
1264 i++;
1265 }
1266 fireTableDataChanged();
1267 selectionModel.clearSelection();
1268 }
1269
[11387]1270 /**
1271 * Sorts paths lexicographically.
1272 */
[2400]1273 protected void sort() {
[10619]1274 data.sort((o1, o2) -> {
1275 if (o1.isEmpty() && o2.isEmpty())
1276 return 0;
1277 if (o1.isEmpty()) return 1;
1278 if (o2.isEmpty()) return -1;
1279 return o1.compareTo(o2);
1280 });
[2400]1281 }
1282
[11387]1283 /**
1284 * Returns the icon paths.
1285 * @return the icon paths
1286 */
[2400]1287 public List<String> getIconPaths() {
[7005]1288 return new ArrayList<>(data);
[2400]1289 }
1290 }
1291
1292 class NewIconPathAction extends AbstractAction {
[8836]1293 NewIconPathAction() {
[2400]1294 putValue(NAME, tr("New"));
1295 putValue(SHORT_DESCRIPTION, tr("Add a new icon path"));
[10482]1296 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
[2400]1297 }
1298
[6084]1299 @Override
[2400]1300 public void actionPerformed(ActionEvent e) {
1301 iconPathsModel.addPath("");
[8510]1302 tblIconPaths.editCellAt(iconPathsModel.getRowCount() -1, 0);
[2400]1303 }
1304 }
1305
1306 class RemoveIconPathAction extends AbstractAction implements ListSelectionListener {
[8836]1307 RemoveIconPathAction() {
[2400]1308 putValue(NAME, tr("Remove"));
1309 putValue(SHORT_DESCRIPTION, tr("Remove the selected icon paths"));
[10482]1310 new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
[2400]1311 updateEnabledState();
1312 }
1313
[6890]1314 protected final void updateEnabledState() {
[2400]1315 setEnabled(tblIconPaths.getSelectedRowCount() > 0);
1316 }
1317
[6084]1318 @Override
[2400]1319 public void valueChanged(ListSelectionEvent e) {
1320 updateEnabledState();
1321 }
1322
[6084]1323 @Override
[2400]1324 public void actionPerformed(ActionEvent e) {
1325 iconPathsModel.removeSelected();
1326 }
1327 }
1328
1329 class EditIconPathAction extends AbstractAction implements ListSelectionListener {
[8836]1330 EditIconPathAction() {
[2400]1331 putValue(NAME, tr("Edit"));
1332 putValue(SHORT_DESCRIPTION, tr("Edit the selected icon path"));
[10482]1333 new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this);
[2400]1334 updateEnabledState();
1335 }
1336
[6890]1337 protected final void updateEnabledState() {
[2400]1338 setEnabled(tblIconPaths.getSelectedRowCount() == 1);
1339 }
1340
[6084]1341 @Override
[2400]1342 public void valueChanged(ListSelectionEvent e) {
1343 updateEnabledState();
1344 }
1345
[6084]1346 @Override
[2400]1347 public void actionPerformed(ActionEvent e) {
1348 int row = tblIconPaths.getSelectedRow();
1349 tblIconPaths.editCellAt(row, 0);
1350 }
1351 }
1352
[7001]1353 static class SourceEntryListCellRenderer extends JLabel implements ListCellRenderer<ExtendedSourceEntry> {
[8507]1354
1355 private final ImageIcon GREEN_CHECK = ImageProvider.getIfAvailable("misc", "green_check");
1356 private final ImageIcon GRAY_CHECK = ImageProvider.getIfAvailable("misc", "gray_check");
1357 private final Map<String, SourceEntry> entryByUrl = new HashMap<>();
1358
[6084]1359 @Override
[7082]1360 public Component getListCellRendererComponent(JList<? extends ExtendedSourceEntry> list, ExtendedSourceEntry value,
[7001]1361 int index, boolean isSelected, boolean cellHasFocus) {
[1743]1362 String s = value.toString();
1363 setText(s);
1364 if (isSelected) {
1365 setBackground(list.getSelectionBackground());
1366 setForeground(list.getSelectionForeground());
[2400]1367 } else {
[1743]1368 setBackground(list.getBackground());
1369 setForeground(list.getForeground());
1370 }
1371 setEnabled(list.isEnabled());
1372 setFont(list.getFont());
[3796]1373 setFont(getFont().deriveFont(Font.PLAIN));
[1743]1374 setOpaque(true);
[7001]1375 setToolTipText(value.getTooltip());
[8507]1376 final SourceEntry sourceEntry = entryByUrl.get(value.url);
1377 setIcon(sourceEntry == null ? null : sourceEntry.active ? GREEN_CHECK : GRAY_CHECK);
[1743]1378 return this;
1379 }
[8507]1380
1381 public void updateSources(List<SourceEntry> sources) {
1382 synchronized (entryByUrl) {
1383 entryByUrl.clear();
1384 for (SourceEntry i : sources) {
1385 entryByUrl.put(i.url, i);
1386 }
1387 }
1388 }
[1743]1389 }
1390
[3797]1391 class SourceLoader extends PleaseWaitRunnable {
[4839]1392 private final String url;
[8318]1393 private final List<SourceProvider> sourceProviders;
[9977]1394 private CachedFile cachedFile;
1395 private boolean canceled;
[7005]1396 private final List<ExtendedSourceEntry> sources = new ArrayList<>();
[1743]1397
[8836]1398 SourceLoader(String url, List<SourceProvider> sourceProviders) {
[3796]1399 super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url));
[2400]1400 this.url = url;
[4839]1401 this.sourceProviders = sourceProviders;
[2400]1402 }
[1743]1403
[2400]1404 @Override
1405 protected void cancel() {
1406 canceled = true;
[9411]1407 Utils.close(cachedFile);
[1865]1408 }
[1743]1409
[2400]1410 protected void warn(Exception e) {
[9570]1411 String emsg = Utils.escapeReservedCharactersHTML(e.getMessage() != null ? e.getMessage() : e.toString());
[7434]1412 final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg);
[2400]1413
[10611]1414 GuiHelper.runInEDT(() -> HelpAwareOptionPane.showOptionDialog(
1415 Main.parent,
1416 msg,
1417 tr("Error"),
1418 JOptionPane.ERROR_MESSAGE,
1419 ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC))
1420 ));
[2400]1421 }
1422
1423 @Override
1424 protected void realRun() throws SAXException, IOException, OsmTransferException {
1425 try {
[3797]1426 sources.addAll(getDefault());
[4874]1427
[4839]1428 for (SourceProvider provider : sourceProviders) {
1429 for (SourceEntry src : provider.getSources()) {
1430 if (src instanceof ExtendedSourceEntry) {
1431 sources.add((ExtendedSourceEntry) src);
1432 }
1433 }
1434 }
[9411]1435 readFile();
[9965]1436 for (Iterator<ExtendedSourceEntry> it = sources.iterator(); it.hasNext();) {
1437 if ("xml".equals(it.next().styleType)) {
[12620]1438 Logging.debug("Removing XML source entry");
[9965]1439 it.remove();
1440 }
1441 }
[9411]1442 } catch (IOException e) {
1443 if (canceled)
1444 // ignore the exception and return
1445 return;
1446 OsmTransferException ex = new OsmTransferException(e);
1447 ex.setUrl(url);
1448 warn(ex);
1449 }
1450 }
[4874]1451
[9411]1452 protected void readFile() throws IOException {
1453 final String lang = LanguageInfo.getLanguageCodeXML();
1454 cachedFile = new CachedFile(url);
[10733]1455 try (BufferedReader reader = cachedFile.getContentReader()) {
[1743]1456
[2400]1457 String line;
[3797]1458 ExtendedSourceEntry last = null;
[2400]1459
1460 while ((line = reader.readLine()) != null && !canceled) {
[6087]1461 if (line.trim().isEmpty()) {
[2400]1462 continue; // skip empty lines
[1865]1463 }
[2400]1464 if (line.startsWith("\t")) {
1465 Matcher m = Pattern.compile("^\t([^:]+): *(.+)$").matcher(line);
[8444]1466 if (!m.matches()) {
[12620]1467 Logging.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
[2400]1468 continue;
1469 }
1470 if (last != null) {
1471 String key = m.group(1);
1472 String value = m.group(2);
1473 if ("author".equals(key) && last.author == null) {
1474 last.author = value;
1475 } else if ("version".equals(key)) {
1476 last.version = value;
1477 } else if ("link".equals(key) && last.link == null) {
1478 last.link = value;
1479 } else if ("description".equals(key) && last.description == null) {
1480 last.description = value;
[3894]1481 } else if ((lang + "shortdescription").equals(key) && last.title == null) {
1482 last.title = value;
1483 } else if ("shortdescription".equals(key) && last.title == null) {
1484 last.title = value;
[3930]1485 } else if ((lang + "title").equals(key) && last.title == null) {
1486 last.title = value;
1487 } else if ("title".equals(key) && last.title == null) {
1488 last.title = value;
[3796]1489 } else if ("name".equals(key) && last.name == null) {
1490 last.name = value;
[2400]1491 } else if ((lang + "author").equals(key)) {
1492 last.author = value;
1493 } else if ((lang + "link").equals(key)) {
1494 last.link = value;
1495 } else if ((lang + "description").equals(key)) {
1496 last.description = value;
[5586]1497 } else if ("min-josm-version".equals(key)) {
1498 try {
[8390]1499 last.minJosmVersion = Integer.valueOf(value);
[5586]1500 } catch (NumberFormatException e) {
1501 // ignore
[12620]1502 Logging.trace(e);
[5586]1503 }
[9334]1504 } else if ("style-type".equals(key)) {
1505 last.styleType = value;
[2400]1506 }
1507 }
[1865]1508 } else {
[2400]1509 last = null;
1510 Matcher m = Pattern.compile("^(.+);(.+)$").matcher(line);
1511 if (m.matches()) {
[12825]1512 last = new ExtendedSourceEntry(sourceType, m.group(1), m.group(2));
[10179]1513 sources.add(last);
[2400]1514 } else {
[12620]1515 Logging.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
[1865]1516 }
[1743]1517 }
1518 }
[2400]1519 }
[4727]1520 }
1521
1522 @Override
1523 protected void finish() {
[5105]1524 Collections.sort(sources);
[3797]1525 availableSourcesModel.setSources(sources);
[1743]1526 }
[2400]1527 }
[4072]1528
[4874]1529 static class SourceEntryTableCellRenderer extends DefaultTableCellRenderer {
[3796]1530 @Override
1531 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
[4072]1532 if (value == null)
1533 return this;
[7024]1534 return super.getTableCellRendererComponent(table,
1535 fromSourceEntry((SourceEntry) value), isSelected, hasFocus, row, column);
[3796]1536 }
[1743]1537
[8870]1538 private static String fromSourceEntry(SourceEntry entry) {
[4041]1539 if (entry == null)
1540 return null;
[10242]1541 StringBuilder s = new StringBuilder(128).append("<html><b>");
[3894]1542 if (entry.title != null) {
[11848]1543 s.append(Utils.escapeReservedCharactersHTML(entry.title)).append("</b> <span color=\"gray\">");
[3796]1544 }
1545 s.append(entry.url);
[3894]1546 if (entry.title != null) {
[5105]1547 s.append("</span>");
[3796]1548 }
1549 s.append("</html>");
1550 return s.toString();
1551 }
1552 }
1553
[2400]1554 class FileOrUrlCellEditor extends JPanel implements TableCellEditor {
[10179]1555 private final JosmTextField tfFileName = new JosmTextField();
[9078]1556 private final CopyOnWriteArrayList<CellEditorListener> listeners;
[2400]1557 private String value;
[9078]1558 private final boolean isFile;
[2400]1559
1560 /**
1561 * build the GUI
1562 */
[6890]1563 protected final void build() {
[2400]1564 setLayout(new GridBagLayout());
1565 GridBagConstraints gc = new GridBagConstraints();
1566 gc.gridx = 0;
1567 gc.gridy = 0;
1568 gc.fill = GridBagConstraints.BOTH;
1569 gc.weightx = 1.0;
1570 gc.weighty = 1.0;
[10179]1571 add(tfFileName, gc);
[2400]1572
1573 gc.gridx = 1;
1574 gc.gridy = 0;
1575 gc.fill = GridBagConstraints.BOTH;
1576 gc.weightx = 0.0;
1577 gc.weighty = 1.0;
1578 add(new JButton(new LaunchFileChooserAction()));
1579
1580 tfFileName.addFocusListener(
1581 new FocusAdapter() {
1582 @Override
1583 public void focusGained(FocusEvent e) {
1584 tfFileName.selectAll();
1585 }
[1865]1586 }
[4727]1587 );
[2400]1588 }
[1743]1589
[8836]1590 FileOrUrlCellEditor(boolean isFile) {
[3308]1591 this.isFile = isFile;
[7005]1592 listeners = new CopyOnWriteArrayList<>();
[2400]1593 build();
1594 }
1595
[6084]1596 @Override
[2400]1597 public void addCellEditorListener(CellEditorListener l) {
[2655]1598 if (l != null) {
1599 listeners.addIfAbsent(l);
[1743]1600 }
[2400]1601 }
[1743]1602
[2400]1603 protected void fireEditingCanceled() {
1604 for (CellEditorListener l: listeners) {
1605 l.editingCanceled(new ChangeEvent(this));
[1743]1606 }
[2400]1607 }
[1743]1608
[2400]1609 protected void fireEditingStopped() {
1610 for (CellEditorListener l: listeners) {
1611 l.editingStopped(new ChangeEvent(this));
[1747]1612 }
[2400]1613 }
[1747]1614
[6084]1615 @Override
[2400]1616 public void cancelCellEditing() {
1617 fireEditingCanceled();
1618 }
[1743]1619
[6084]1620 @Override
[2400]1621 public Object getCellEditorValue() {
1622 return value;
[1743]1623 }
1624
[6084]1625 @Override
[2400]1626 public boolean isCellEditable(EventObject anEvent) {
1627 if (anEvent instanceof MouseEvent)
[8510]1628 return ((MouseEvent) anEvent).getClickCount() >= 2;
[10293]1629 return true;
[2400]1630 }
1631
[6084]1632 @Override
[2400]1633 public void removeCellEditorListener(CellEditorListener l) {
[2655]1634 listeners.remove(l);
[1743]1635 }
[2400]1636
[6084]1637 @Override
[2400]1638 public boolean shouldSelectCell(EventObject anEvent) {
1639 return true;
[1865]1640 }
[2400]1641
[6084]1642 @Override
[2400]1643 public boolean stopCellEditing() {
1644 value = tfFileName.getText();
1645 fireEditingStopped();
1646 return true;
1647 }
1648
1649 public void setInitialValue(String initialValue) {
1650 this.value = initialValue;
1651 if (initialValue == null) {
1652 this.tfFileName.setText("");
1653 } else {
1654 this.tfFileName.setText(initialValue);
[1743]1655 }
1656 }
1657
[6084]1658 @Override
[2400]1659 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
[8510]1660 setInitialValue((String) value);
[2400]1661 tfFileName.selectAll();
1662 return this;
1663 }
1664
1665 class LaunchFileChooserAction extends AbstractAction {
[8836]1666 LaunchFileChooserAction() {
[2400]1667 putValue(NAME, "...");
1668 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
[1743]1669 }
1670
[6084]1671 @Override
[2400]1672 public void actionPerformed(ActionEvent e) {
[7578]1673 FileChooserManager fcm = new FileChooserManager(true).createFileChooser();
[5438]1674 if (!isFile) {
1675 fcm.getFileChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1676 }
1677 prepareFileChooser(tfFileName.getText(), fcm.getFileChooser());
[10035]1678 AbstractFileChooser fc = fcm.openFileChooser(GuiHelper.getFrameForComponent(SourceEditor.this));
[5438]1679 if (fc != null) {
1680 tfFileName.setText(fc.getSelectedFile().toString());
1681 }
[2400]1682 }
[1743]1683 }
1684 }
[3028]1685
[11367]1686 /**
[6670]1687 * Defers loading of sources to the first time the adequate tab is selected.
1688 * @param tab The preferences tab
1689 * @param component The tab component
1690 * @since 6670
1691 */
1692 public final void deferLoading(final DefaultTabPreferenceSetting tab, final Component component) {
[10611]1693 tab.getTabPane().addChangeListener(e -> {
1694 if (tab.getTabPane().getSelectedComponent() == component) {
[10634]1695 initiallyLoadAvailableSources();
[10611]1696 }
1697 });
[6670]1698 }
[8835]1699
[11387]1700 /**
1701 * Returns the title of the given source entry.
1702 * @param entry source entry
1703 * @return the title of the given source entry, or null if empty
1704 */
[8835]1705 protected String getTitleForSourceEntry(SourceEntry entry) {
1706 return "".equals(entry.title) ? null : entry.title;
1707 }
[1743]1708}
Note: See TracBrowser for help on using the repository browser.