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