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

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

checkstyle + javadoc

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