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

Last change on this file since 11848 was 11848, checked in by Don-vip, 7 years ago

fix #14613 - Special HTML characters not escaped in GUI error messages

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