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

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

add unit test to check syntax validity of all MapCSSTagChecker rules

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