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