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

Last change on this file since 12669 was 12649, checked in by Don-vip, 7 years ago

see #15182 - code refactoring to avoid dependence on GUI packages from Preferences

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