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

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

fix #15572 - use ImageProvider attach API for all JOSM actions to ensure proper icon size everywhere

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