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

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

see #19334 - javadoc fixes + protected constructors for abstract classes

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