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

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

see #15182 - deprecate Main.worker, replace it by gui.MainApplication.worker + code refactoring to make sure only editor packages use it

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