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

Last change on this file since 17161 was 17161, checked in by simon04, 4 years ago

see #7548 - Re-organize the preference dialog (map preferences)

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