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