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