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

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

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

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