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

Last change on this file since 10482 was 10482, checked in by stoecker, 8 years ago

see #12994 - don't warn about SideButton without icon

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