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

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

global use of Utils.isEmpty/isBlank

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