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