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