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

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

respect default preferences when writing preset/style/rule entries

This fixes request for restart with fresh preferences despite no changes in the preferences window.

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