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, Preset and TagChecker Rule) 419 * @param ident any {@link I18nString} value 420 * @return the translated string for {@code ident} 421 */ 422 protected abstract String getStr(I18nString ident); 423 424 /** 425 * Identifiers for strings that need to be provided. 426 */ 427 public enum I18nString { 428 /** Available (styles|presets|rules) */ 429 AVAILABLE_SOURCES, 430 /** Active (styles|presets|rules) */ 431 ACTIVE_SOURCES, 432 /** Add a new (style|preset|rule) by entering filename or URL */ 433 NEW_SOURCE_ENTRY_TOOLTIP, 434 /** New (style|preset|rule) entry */ 435 NEW_SOURCE_ENTRY, 436 /** Remove the selected (styles|presets|rules) from the list of active (styles|presets|rules) */ 437 REMOVE_SOURCE_TOOLTIP, 438 /** Edit the filename or URL for the selected active (style|preset|rule) */ 439 EDIT_SOURCE_TOOLTIP, 440 /** Add the selected available (styles|presets|rules) to the list of active (styles|presets|rules) */ 441 ACTIVATE_TOOLTIP, 442 /** Reloads the list of available (styles|presets|rules) */ 443 RELOAD_ALL_AVAILABLE, 444 /** Loading (style|preset|rule) sources */ 445 LOADING_SOURCES_FROM, 446 /** Failed to load the list of (style|preset|rule) sources */ 447 FAILED_TO_LOAD_SOURCES_FROM, 448 /** /Preferences/(Styles|Presets|Rules)#FailedToLoad(Style|Preset|Rule)Sources */ 449 FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC, 450 /** Illegal format of entry in (style|preset|rule) list */ 451 ILLEGAL_FORMAT_OF_ENTRY 452 } 453 454 public boolean hasActiveSourcesChanged() { 455 Collection<? extends SourceEntry> prev = getInitialSourcesList(); 456 List<SourceEntry> cur = activeSourcesModel.getSources(); 457 if (prev.size() != cur.size()) 458 return true; 459 Iterator<? extends SourceEntry> p = prev.iterator(); 460 Iterator<SourceEntry> c = cur.iterator(); 461 while (p.hasNext()) { 462 SourceEntry pe = p.next(); 463 SourceEntry ce = c.next(); 464 if (!Objects.equals(pe.url, ce.url) || !Objects.equals(pe.name, ce.name) || pe.active != ce.active) 465 return true; 466 } 467 return false; 468 } 469 470 public Collection<SourceEntry> getActiveSources() { 471 return activeSourcesModel.getSources(); 472 } 473 474 /** 475 * Synchronously loads available sources and returns the parsed list. 476 * @return list of available sources 477 */ 478 public final Collection<ExtendedSourceEntry> loadAndGetAvailableSources() { 479 try { 480 final SourceLoader loader = new SourceLoader(availableSourcesUrl, sourceProviders); 481 loader.realRun(); 482 return loader.sources; 483 } catch (Exception ex) { 484 throw new RuntimeException(ex); 485 } 486 } 487 488 public void removeSources(Collection<Integer> idxs) { 489 activeSourcesModel.removeIdxs(idxs); 490 } 491 492 protected void reloadAvailableSources(String url, List<SourceProvider> sourceProviders) { 493 Main.worker.submit(new SourceLoader(url, sourceProviders)); 494 } 495 496 /** 497 * Performs the initial loading of source providers. Does nothing if already done. 498 */ 499 public void initiallyLoadAvailableSources() { 500 if (!sourcesInitiallyLoaded) { 501 reloadAvailableSources(availableSourcesUrl, sourceProviders); 502 } 503 sourcesInitiallyLoaded = true; 504 } 505 506 protected static class AvailableSourcesListModel extends DefaultListModel<ExtendedSourceEntry> { 507 private final transient List<ExtendedSourceEntry> data; 508 private final DefaultListSelectionModel selectionModel; 509 510 public AvailableSourcesListModel(DefaultListSelectionModel selectionModel) { 511 data = new ArrayList<>(); 512 this.selectionModel = selectionModel; 513 } 514 515 public void setSources(List<ExtendedSourceEntry> sources) { 516 data.clear(); 517 if (sources != null) { 518 data.addAll(sources); 519 } 520 fireContentsChanged(this, 0, data.size()); 521 } 522 523 @Override 524 public ExtendedSourceEntry getElementAt(int index) { 525 return data.get(index); 526 } 527 528 @Override 529 public int getSize() { 530 if (data == null) return 0; 531 return data.size(); 532 } 533 534 public void deleteSelected() { 535 Iterator<ExtendedSourceEntry> it = data.iterator(); 536 int i = 0; 537 while (it.hasNext()) { 538 it.next(); 539 if (selectionModel.isSelectedIndex(i)) { 540 it.remove(); 541 } 542 i++; 543 } 544 fireContentsChanged(this, 0, data.size()); 545 } 546 547 public List<ExtendedSourceEntry> getSelected() { 548 List<ExtendedSourceEntry> ret = new ArrayList<>(); 549 for (int i = 0; i < data.size(); i++) { 550 if (selectionModel.isSelectedIndex(i)) { 551 ret.add(data.get(i)); 552 } 553 } 554 return ret; 555 } 556 } 557 558 protected class ActiveSourcesModel extends AbstractTableModel { 559 private transient List<SourceEntry> data; 560 private final DefaultListSelectionModel selectionModel; 561 562 public ActiveSourcesModel(DefaultListSelectionModel selectionModel) { 563 this.selectionModel = selectionModel; 564 this.data = new ArrayList<>(); 565 } 566 567 @Override 568 public int getColumnCount() { 569 return canEnable ? 2 : 1; 570 } 571 572 @Override 573 public int getRowCount() { 574 return data == null ? 0 : data.size(); 575 } 576 577 @Override 578 public Object getValueAt(int rowIndex, int columnIndex) { 579 if (canEnable && columnIndex == 0) 580 return data.get(rowIndex).active; 581 else 582 return data.get(rowIndex); 583 } 584 585 @Override 586 public boolean isCellEditable(int rowIndex, int columnIndex) { 587 return canEnable && columnIndex == 0; 588 } 589 590 @Override 591 public Class<?> getColumnClass(int column) { 592 if (canEnable && column == 0) 593 return Boolean.class; 594 else return SourceEntry.class; 595 } 596 597 @Override 598 public void setValueAt(Object aValue, int row, int column) { 599 if (row < 0 || row >= getRowCount() || aValue == null) 600 return; 601 if (canEnable && column == 0) { 602 data.get(row).active = !data.get(row).active; 603 } 604 } 605 606 public void setActiveSources(Collection<? extends SourceEntry> sources) { 607 data.clear(); 608 if (sources != null) { 609 for (SourceEntry e : sources) { 610 data.add(new SourceEntry(e)); 611 } 612 } 613 fireTableDataChanged(); 614 } 615 616 public void addSource(SourceEntry entry) { 617 if (entry == null) return; 618 data.add(entry); 619 fireTableDataChanged(); 620 int idx = data.indexOf(entry); 621 if (idx >= 0) { 622 selectionModel.setSelectionInterval(idx, idx); 623 } 624 } 625 626 public void removeSelected() { 627 Iterator<SourceEntry> it = data.iterator(); 628 int i = 0; 629 while (it.hasNext()) { 630 it.next(); 631 if (selectionModel.isSelectedIndex(i)) { 632 it.remove(); 633 } 634 i++; 635 } 636 fireTableDataChanged(); 637 } 638 639 public void removeIdxs(Collection<Integer> idxs) { 640 List<SourceEntry> newData = new ArrayList<>(); 641 for (int i = 0; i < data.size(); ++i) { 642 if (!idxs.contains(i)) { 643 newData.add(data.get(i)); 644 } 645 } 646 data = newData; 647 fireTableDataChanged(); 648 } 649 650 public void addExtendedSourceEntries(List<ExtendedSourceEntry> sources) { 651 if (sources == null) return; 652 for (ExtendedSourceEntry info: sources) { 653 data.add(new SourceEntry(info.url, info.name, info.getDisplayName(), true)); 654 } 655 fireTableDataChanged(); 656 selectionModel.clearSelection(); 657 for (ExtendedSourceEntry info: sources) { 658 int pos = data.indexOf(info); 659 if (pos >= 0) { 660 selectionModel.addSelectionInterval(pos, pos); 661 } 662 } 663 } 664 665 public List<SourceEntry> getSources() { 666 return new ArrayList<>(data); 667 } 668 669 public boolean canMove(int i) { 670 int[] sel = tblActiveSources.getSelectedRows(); 671 if (sel.length == 0) 672 return false; 673 if (i < 0) 674 return sel[0] >= -i; 675 else if (i > 0) 676 return sel[sel.length-1] <= getRowCount()-1 - i; 677 else 678 return true; 679 } 680 681 public void move(int i) { 682 if (!canMove(i)) return; 683 int[] sel = tblActiveSources.getSelectedRows(); 684 for (int row: sel) { 685 SourceEntry t1 = data.get(row); 686 SourceEntry t2 = data.get(row + i); 687 data.set(row, t2); 688 data.set(row + i, t1); 689 } 690 selectionModel.clearSelection(); 691 for (int row: sel) { 692 selectionModel.addSelectionInterval(row + i, row + i); 693 } 694 } 695 } 696 697 public static class ExtendedSourceEntry extends SourceEntry implements Comparable<ExtendedSourceEntry> { 698 public String simpleFileName; 699 public String version; 700 public String author; 701 public String link; 702 public String description; 703 public Integer minJosmVersion; 704 705 public ExtendedSourceEntry(String simpleFileName, String url) { 706 super(url, null, null, true); 707 this.simpleFileName = simpleFileName; 708 } 709 710 /** 711 * @return string representation for GUI list or menu entry 712 */ 713 public String getDisplayName() { 714 return title == null ? simpleFileName : title; 715 } 716 717 private static void appendRow(StringBuilder s, String th, String td) { 718 s.append("<tr><th>").append(th).append("</th><td>").append(td).append("</td</tr>"); 719 } 720 721 public String getTooltip() { 722 StringBuilder s = new StringBuilder(); 723 appendRow(s, tr("Short Description:"), getDisplayName()); 724 appendRow(s, tr("URL:"), url); 725 if (author != null) { 726 appendRow(s, tr("Author:"), author); 727 } 728 if (link != null) { 729 appendRow(s, tr("Webpage:"), link); 730 } 731 if (description != null) { 732 appendRow(s, tr("Description:"), description); 733 } 734 if (version != null) { 735 appendRow(s, tr("Version:"), version); 736 } 737 if (minJosmVersion != null) { 738 appendRow(s, tr("Minimum JOSM Version:"), Integer.toString(minJosmVersion)); 739 } 740 return "<html><style>th{text-align:right}td{width:400px}</style>" 741 + "<table>" + s + "</table></html>"; 742 } 743 744 @Override 745 public String toString() { 746 return "<html><b>" + getDisplayName() + "</b>" 747 + (author == null ? "" : " <span color=\"gray\">" + tr("by {0}", author) + "</color>") 748 + "</html>"; 749 } 750 751 @Override 752 public int compareTo(ExtendedSourceEntry o) { 753 if (url.startsWith("resource") && !o.url.startsWith("resource")) 754 return -1; 755 if (o.url.startsWith("resource")) 756 return 1; 757 else 758 return getDisplayName().compareToIgnoreCase(o.getDisplayName()); 759 } 760 } 761 762 private static void prepareFileChooser(String url, AbstractFileChooser fc) { 763 if (url == null || url.trim().isEmpty()) return; 764 URL sourceUrl = null; 765 try { 766 sourceUrl = new URL(url); 767 } catch (MalformedURLException e) { 768 File f = new File(url); 769 if (f.isFile()) { 770 f = f.getParentFile(); 771 } 772 if (f != null) { 773 fc.setCurrentDirectory(f); 774 } 775 return; 776 } 777 if (sourceUrl.getProtocol().startsWith("file")) { 778 File f = new File(sourceUrl.getPath()); 779 if (f.isFile()) { 780 f = f.getParentFile(); 781 } 782 if (f != null) { 783 fc.setCurrentDirectory(f); 784 } 785 } 786 } 787 788 protected class EditSourceEntryDialog extends ExtendedDialog { 789 790 private final JosmTextField tfTitle; 791 private final JosmTextField tfURL; 792 private JCheckBox cbActive; 793 794 public EditSourceEntryDialog(Component parent, String title, SourceEntry e) { 795 super(parent, title, new String[] {tr("Ok"), tr("Cancel")}); 796 797 JPanel p = new JPanel(new GridBagLayout()); 798 799 tfTitle = new JosmTextField(60); 800 p.add(new JLabel(tr("Name (optional):")), GBC.std().insets(15, 0, 5, 5)); 801 p.add(tfTitle, GBC.eol().insets(0, 0, 5, 5)); 802 803 tfURL = new JosmTextField(60); 804 p.add(new JLabel(tr("URL / File:")), GBC.std().insets(15, 0, 5, 0)); 805 p.add(tfURL, GBC.std().insets(0, 0, 5, 5)); 806 JButton fileChooser = new JButton(new LaunchFileChooserAction()); 807 fileChooser.setMargin(new Insets(0, 0, 0, 0)); 808 p.add(fileChooser, GBC.eol().insets(0, 0, 5, 5)); 809 810 if (e != null) { 811 if (e.title != null) { 812 tfTitle.setText(e.title); 813 } 814 tfURL.setText(e.url); 815 } 816 817 if (canEnable) { 818 cbActive = new JCheckBox(tr("active"), e == null || e.active); 819 p.add(cbActive, GBC.eol().insets(15, 0, 5, 0)); 820 } 821 setButtonIcons(new String[] {"ok", "cancel"}); 822 setContent(p); 823 824 // Make OK button enabled only when a file/URL has been set 825 tfURL.getDocument().addDocumentListener(new DocumentListener() { 826 @Override 827 public void insertUpdate(DocumentEvent e) { 828 updateOkButtonState(); 829 } 830 831 @Override 832 public void removeUpdate(DocumentEvent e) { 833 updateOkButtonState(); 834 } 835 836 @Override 837 public void changedUpdate(DocumentEvent e) { 838 updateOkButtonState(); 839 } 840 }); 841 } 842 843 private void updateOkButtonState() { 844 buttons.get(0).setEnabled(!Utils.strip(tfURL.getText()).isEmpty()); 845 } 846 847 @Override 848 public void setupDialog() { 849 super.setupDialog(); 850 updateOkButtonState(); 851 } 852 853 class LaunchFileChooserAction extends AbstractAction { 854 LaunchFileChooserAction() { 855 putValue(SMALL_ICON, ImageProvider.get("open")); 856 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file")); 857 } 858 859 @Override 860 public void actionPerformed(ActionEvent e) { 861 FileFilter ff; 862 switch (sourceType) { 863 case MAP_PAINT_STYLE: 864 ff = new ExtensionFileFilter("xml,mapcss,css,zip", "xml", tr("Map paint style file (*.xml, *.mapcss, *.zip)")); 865 break; 866 case TAGGING_PRESET: 867 ff = new ExtensionFileFilter("xml,zip", "xml", tr("Preset definition file (*.xml, *.zip)")); 868 break; 869 case TAGCHECKER_RULE: 870 ff = new ExtensionFileFilter("validator.mapcss,zip", "validator.mapcss", tr("Tag checker rule (*.validator.mapcss, *.zip)")); 871 break; 872 default: 873 Main.error("Unsupported source type: "+sourceType); 874 return; 875 } 876 FileChooserManager fcm = new FileChooserManager(true) 877 .createFileChooser(true, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY); 878 prepareFileChooser(tfURL.getText(), fcm.getFileChooser()); 879 AbstractFileChooser fc = fcm.openFileChooser(JOptionPane.getFrameForComponent(SourceEditor.this)); 880 if (fc != null) { 881 tfURL.setText(fc.getSelectedFile().toString()); 882 } 883 } 884 } 885 886 @Override 887 public String getTitle() { 888 return tfTitle.getText(); 889 } 890 891 public String getURL() { 892 return tfURL.getText(); 893 } 894 895 public boolean active() { 896 if (!canEnable) 897 throw new UnsupportedOperationException(); 898 return cbActive.isSelected(); 899 } 900 } 901 902 class NewActiveSourceAction extends AbstractAction { 903 NewActiveSourceAction() { 904 putValue(NAME, tr("New")); 905 putValue(SHORT_DESCRIPTION, getStr(I18nString.NEW_SOURCE_ENTRY_TOOLTIP)); 906 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 907 } 908 909 @Override 910 public void actionPerformed(ActionEvent evt) { 911 EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog( 912 SourceEditor.this, 913 getStr(I18nString.NEW_SOURCE_ENTRY), 914 null); 915 editEntryDialog.showDialog(); 916 if (editEntryDialog.getValue() == 1) { 917 boolean active = true; 918 if (canEnable) { 919 active = editEntryDialog.active(); 920 } 921 final SourceEntry entry = new SourceEntry( 922 editEntryDialog.getURL(), 923 null, editEntryDialog.getTitle(), active); 924 entry.title = getTitleForSourceEntry(entry); 925 activeSourcesModel.addSource(entry); 926 activeSourcesModel.fireTableDataChanged(); 927 } 928 } 929 } 930 931 class RemoveActiveSourcesAction extends AbstractAction implements ListSelectionListener { 932 933 RemoveActiveSourcesAction() { 934 putValue(NAME, tr("Remove")); 935 putValue(SHORT_DESCRIPTION, getStr(I18nString.REMOVE_SOURCE_TOOLTIP)); 936 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 937 updateEnabledState(); 938 } 939 940 protected final void updateEnabledState() { 941 setEnabled(tblActiveSources.getSelectedRowCount() > 0); 942 } 943 944 @Override 945 public void valueChanged(ListSelectionEvent e) { 946 updateEnabledState(); 947 } 948 949 @Override 950 public void actionPerformed(ActionEvent e) { 951 activeSourcesModel.removeSelected(); 952 } 953 } 954 955 class EditActiveSourceAction extends AbstractAction implements ListSelectionListener { 956 EditActiveSourceAction() { 957 putValue(NAME, tr("Edit")); 958 putValue(SHORT_DESCRIPTION, getStr(I18nString.EDIT_SOURCE_TOOLTIP)); 959 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 960 updateEnabledState(); 961 } 962 963 protected final void updateEnabledState() { 964 setEnabled(tblActiveSources.getSelectedRowCount() == 1); 965 } 966 967 @Override 968 public void valueChanged(ListSelectionEvent e) { 969 updateEnabledState(); 970 } 971 972 @Override 973 public void actionPerformed(ActionEvent evt) { 974 int pos = tblActiveSources.getSelectedRow(); 975 if (pos < 0 || pos >= tblActiveSources.getRowCount()) 976 return; 977 978 SourceEntry e = (SourceEntry) activeSourcesModel.getValueAt(pos, 1); 979 980 EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog( 981 SourceEditor.this, tr("Edit source entry:"), e); 982 editEntryDialog.showDialog(); 983 if (editEntryDialog.getValue() == 1) { 984 if (e.title != null || !"".equals(editEntryDialog.getTitle())) { 985 e.title = editEntryDialog.getTitle(); 986 e.title = getTitleForSourceEntry(e); 987 } 988 e.url = editEntryDialog.getURL(); 989 if (canEnable) { 990 e.active = editEntryDialog.active(); 991 } 992 activeSourcesModel.fireTableRowsUpdated(pos, pos); 993 } 994 } 995 } 996 997 /** 998 * The action to move the currently selected entries up or down in the list. 999 */ 1000 class MoveUpDownAction extends AbstractAction implements ListSelectionListener, TableModelListener { 1001 private final int increment; 1002 1003 MoveUpDownAction(boolean isDown) { 1004 increment = isDown ? 1 : -1; 1005 putValue(SMALL_ICON, isDown ? ImageProvider.get("dialogs", "down") : ImageProvider.get("dialogs", "up")); 1006 putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up.")); 1007 updateEnabledState(); 1008 } 1009 1010 public final void updateEnabledState() { 1011 setEnabled(activeSourcesModel.canMove(increment)); 1012 } 1013 1014 @Override 1015 public void actionPerformed(ActionEvent e) { 1016 activeSourcesModel.move(increment); 1017 } 1018 1019 @Override 1020 public void valueChanged(ListSelectionEvent e) { 1021 updateEnabledState(); 1022 } 1023 1024 @Override 1025 public void tableChanged(TableModelEvent e) { 1026 updateEnabledState(); 1027 } 1028 } 1029 1030 class ActivateSourcesAction extends AbstractAction implements ListSelectionListener { 1031 ActivateSourcesAction() { 1032 putValue(SHORT_DESCRIPTION, getStr(I18nString.ACTIVATE_TOOLTIP)); 1033 putValue(SMALL_ICON, ImageProvider.get("preferences", "activate-right")); 1034 updateEnabledState(); 1035 } 1036 1037 protected final void updateEnabledState() { 1038 setEnabled(lstAvailableSources.getSelectedIndices().length > 0); 1039 } 1040 1041 @Override 1042 public void valueChanged(ListSelectionEvent e) { 1043 updateEnabledState(); 1044 } 1045 1046 @Override 1047 public void actionPerformed(ActionEvent e) { 1048 List<ExtendedSourceEntry> sources = availableSourcesModel.getSelected(); 1049 int josmVersion = Version.getInstance().getVersion(); 1050 if (josmVersion != Version.JOSM_UNKNOWN_VERSION) { 1051 Collection<String> messages = new ArrayList<>(); 1052 for (ExtendedSourceEntry entry : sources) { 1053 if (entry.minJosmVersion != null && entry.minJosmVersion > josmVersion) { 1054 messages.add(tr("Entry ''{0}'' requires JOSM Version {1}. (Currently running: {2})", 1055 entry.title, 1056 Integer.toString(entry.minJosmVersion), 1057 Integer.toString(josmVersion)) 1058 ); 1059 } 1060 } 1061 if (!messages.isEmpty()) { 1062 ExtendedDialog dlg = new ExtendedDialog(Main.parent, tr("Warning"), new String[] {tr("Cancel"), tr("Continue anyway")}); 1063 dlg.setButtonIcons(new Icon[] { 1064 ImageProvider.get("cancel"), 1065 new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).addOverlay( 1066 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get() 1067 }); 1068 dlg.setToolTipTexts(new String[] { 1069 tr("Cancel and return to the previous dialog"), 1070 tr("Ignore warning and install style anyway")}); 1071 dlg.setContent("<html>" + tr("Some entries have unmet dependencies:") + 1072 "<br>" + Utils.join("<br>", messages) + "</html>"); 1073 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 1074 if (dlg.showDialog().getValue() != 2) 1075 return; 1076 } 1077 } 1078 activeSourcesModel.addExtendedSourceEntries(sources); 1079 } 1080 } 1081 1082 class ResetAction extends AbstractAction { 1083 1084 ResetAction() { 1085 putValue(NAME, tr("Reset")); 1086 putValue(SHORT_DESCRIPTION, tr("Reset to default")); 1087 putValue(SMALL_ICON, ImageProvider.get("preferences", "reset")); 1088 } 1089 1090 @Override 1091 public void actionPerformed(ActionEvent e) { 1092 activeSourcesModel.setActiveSources(getDefault()); 1093 } 1094 } 1095 1096 class ReloadSourcesAction extends AbstractAction { 1097 private final String url; 1098 private final transient List<SourceProvider> sourceProviders; 1099 1100 ReloadSourcesAction(String url, List<SourceProvider> sourceProviders) { 1101 putValue(NAME, tr("Reload")); 1102 putValue(SHORT_DESCRIPTION, tr(getStr(I18nString.RELOAD_ALL_AVAILABLE), url)); 1103 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh")); 1104 this.url = url; 1105 this.sourceProviders = sourceProviders; 1106 setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE)); 1107 } 1108 1109 @Override 1110 public void actionPerformed(ActionEvent e) { 1111 CachedFile.cleanup(url); 1112 reloadAvailableSources(url, sourceProviders); 1113 } 1114 } 1115 1116 protected static class IconPathTableModel extends AbstractTableModel { 1117 private final List<String> data; 1118 private final DefaultListSelectionModel selectionModel; 1119 1120 public IconPathTableModel(DefaultListSelectionModel selectionModel) { 1121 this.selectionModel = selectionModel; 1122 this.data = new ArrayList<>(); 1123 } 1124 1125 @Override 1126 public int getColumnCount() { 1127 return 1; 1128 } 1129 1130 @Override 1131 public int getRowCount() { 1132 return data == null ? 0 : data.size(); 1133 } 1134 1135 @Override 1136 public Object getValueAt(int rowIndex, int columnIndex) { 1137 return data.get(rowIndex); 1138 } 1139 1140 @Override 1141 public boolean isCellEditable(int rowIndex, int columnIndex) { 1142 return true; 1143 } 1144 1145 @Override 1146 public void setValueAt(Object aValue, int rowIndex, int columnIndex) { 1147 updatePath(rowIndex, (String) aValue); 1148 } 1149 1150 public void setIconPaths(Collection<String> paths) { 1151 data.clear(); 1152 if (paths != null) { 1153 data.addAll(paths); 1154 } 1155 sort(); 1156 fireTableDataChanged(); 1157 } 1158 1159 public void addPath(String path) { 1160 if (path == null) return; 1161 data.add(path); 1162 sort(); 1163 fireTableDataChanged(); 1164 int idx = data.indexOf(path); 1165 if (idx >= 0) { 1166 selectionModel.setSelectionInterval(idx, idx); 1167 } 1168 } 1169 1170 public void updatePath(int pos, String path) { 1171 if (path == null) return; 1172 if (pos < 0 || pos >= getRowCount()) return; 1173 data.set(pos, path); 1174 sort(); 1175 fireTableDataChanged(); 1176 int idx = data.indexOf(path); 1177 if (idx >= 0) { 1178 selectionModel.setSelectionInterval(idx, idx); 1179 } 1180 } 1181 1182 public void removeSelected() { 1183 Iterator<String> it = data.iterator(); 1184 int i = 0; 1185 while (it.hasNext()) { 1186 it.next(); 1187 if (selectionModel.isSelectedIndex(i)) { 1188 it.remove(); 1189 } 1190 i++; 1191 } 1192 fireTableDataChanged(); 1193 selectionModel.clearSelection(); 1194 } 1195 1196 protected void sort() { 1197 Collections.sort( 1198 data, 1199 new Comparator<String>() { 1200 @Override 1201 public int compare(String o1, String o2) { 1202 if (o1.isEmpty() && o2.isEmpty()) 1203 return 0; 1204 if (o1.isEmpty()) return 1; 1205 if (o2.isEmpty()) return -1; 1206 return o1.compareTo(o2); 1207 } 1208 } 1209 ); 1210 } 1211 1212 public List<String> getIconPaths() { 1213 return new ArrayList<>(data); 1214 } 1215 } 1216 1217 class NewIconPathAction extends AbstractAction { 1218 NewIconPathAction() { 1219 putValue(NAME, tr("New")); 1220 putValue(SHORT_DESCRIPTION, tr("Add a new icon path")); 1221 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 1222 } 1223 1224 @Override 1225 public void actionPerformed(ActionEvent e) { 1226 iconPathsModel.addPath(""); 1227 tblIconPaths.editCellAt(iconPathsModel.getRowCount() -1, 0); 1228 } 1229 } 1230 1231 class RemoveIconPathAction extends AbstractAction implements ListSelectionListener { 1232 RemoveIconPathAction() { 1233 putValue(NAME, tr("Remove")); 1234 putValue(SHORT_DESCRIPTION, tr("Remove the selected icon paths")); 1235 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 1236 updateEnabledState(); 1237 } 1238 1239 protected final void updateEnabledState() { 1240 setEnabled(tblIconPaths.getSelectedRowCount() > 0); 1241 } 1242 1243 @Override 1244 public void valueChanged(ListSelectionEvent e) { 1245 updateEnabledState(); 1246 } 1247 1248 @Override 1249 public void actionPerformed(ActionEvent e) { 1250 iconPathsModel.removeSelected(); 1251 } 1252 } 1253 1254 class EditIconPathAction extends AbstractAction implements ListSelectionListener { 1255 EditIconPathAction() { 1256 putValue(NAME, tr("Edit")); 1257 putValue(SHORT_DESCRIPTION, tr("Edit the selected icon path")); 1258 putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit")); 1259 updateEnabledState(); 1260 } 1261 1262 protected final void updateEnabledState() { 1263 setEnabled(tblIconPaths.getSelectedRowCount() == 1); 1264 } 1265 1266 @Override 1267 public void valueChanged(ListSelectionEvent e) { 1268 updateEnabledState(); 1269 } 1270 1271 @Override 1272 public void actionPerformed(ActionEvent e) { 1273 int row = tblIconPaths.getSelectedRow(); 1274 tblIconPaths.editCellAt(row, 0); 1275 } 1276 } 1277 1278 static class SourceEntryListCellRenderer extends JLabel implements ListCellRenderer<ExtendedSourceEntry> { 1279 1280 private final ImageIcon GREEN_CHECK = ImageProvider.getIfAvailable("misc", "green_check"); 1281 private final ImageIcon GRAY_CHECK = ImageProvider.getIfAvailable("misc", "gray_check"); 1282 private final Map<String, SourceEntry> entryByUrl = new HashMap<>(); 1283 1284 @Override 1285 public Component getListCellRendererComponent(JList<? extends ExtendedSourceEntry> list, ExtendedSourceEntry value, 1286 int index, boolean isSelected, boolean cellHasFocus) { 1287 String s = value.toString(); 1288 setText(s); 1289 if (isSelected) { 1290 setBackground(list.getSelectionBackground()); 1291 setForeground(list.getSelectionForeground()); 1292 } else { 1293 setBackground(list.getBackground()); 1294 setForeground(list.getForeground()); 1295 } 1296 setEnabled(list.isEnabled()); 1297 setFont(list.getFont()); 1298 setFont(getFont().deriveFont(Font.PLAIN)); 1299 setOpaque(true); 1300 setToolTipText(value.getTooltip()); 1301 final SourceEntry sourceEntry = entryByUrl.get(value.url); 1302 setIcon(sourceEntry == null ? null : sourceEntry.active ? GREEN_CHECK : GRAY_CHECK); 1303 return this; 1304 } 1305 1306 public void updateSources(List<SourceEntry> sources) { 1307 synchronized (entryByUrl) { 1308 entryByUrl.clear(); 1309 for (SourceEntry i : sources) { 1310 entryByUrl.put(i.url, i); 1311 } 1312 } 1313 } 1314 } 1315 1316 class SourceLoader extends PleaseWaitRunnable { 1317 private final String url; 1318 private final List<SourceProvider> sourceProviders; 1319 private BufferedReader reader; 1320 private boolean canceled; 1321 private final List<ExtendedSourceEntry> sources = new ArrayList<>(); 1322 1323 SourceLoader(String url, List<SourceProvider> sourceProviders) { 1324 super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url)); 1325 this.url = url; 1326 this.sourceProviders = sourceProviders; 1327 } 1328 1329 @Override 1330 protected void cancel() { 1331 canceled = true; 1332 Utils.close(reader); 1333 } 1334 1335 protected void warn(Exception e) { 1336 String emsg = e.getMessage() != null ? e.getMessage() : e.toString(); 1337 emsg = emsg.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); 1338 final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg); 1339 1340 GuiHelper.runInEDT(new Runnable() { 1341 @Override 1342 public void run() { 1343 HelpAwareOptionPane.showOptionDialog( 1344 Main.parent, 1345 msg, 1346 tr("Error"), 1347 JOptionPane.ERROR_MESSAGE, 1348 ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC)) 1349 ); 1350 } 1351 }); 1352 } 1353 1354 @Override 1355 protected void realRun() throws SAXException, IOException, OsmTransferException { 1356 String lang = LanguageInfo.getLanguageCodeXML(); 1357 try { 1358 sources.addAll(getDefault()); 1359 1360 for (SourceProvider provider : sourceProviders) { 1361 for (SourceEntry src : provider.getSources()) { 1362 if (src instanceof ExtendedSourceEntry) { 1363 sources.add((ExtendedSourceEntry) src); 1364 } 1365 } 1366 } 1367 1368 InputStream stream = new CachedFile(url).getInputStream(); 1369 reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); 1370 1371 String line; 1372 ExtendedSourceEntry last = null; 1373 1374 while ((line = reader.readLine()) != null && !canceled) { 1375 if (line.trim().isEmpty()) { 1376 continue; // skip empty lines 1377 } 1378 if (line.startsWith("\t")) { 1379 Matcher m = Pattern.compile("^\t([^:]+): *(.+)$").matcher(line); 1380 if (!m.matches()) { 1381 Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line)); 1382 continue; 1383 } 1384 if (last != null) { 1385 String key = m.group(1); 1386 String value = m.group(2); 1387 if ("author".equals(key) && last.author == null) { 1388 last.author = value; 1389 } else if ("version".equals(key)) { 1390 last.version = value; 1391 } else if ("link".equals(key) && last.link == null) { 1392 last.link = value; 1393 } else if ("description".equals(key) && last.description == null) { 1394 last.description = value; 1395 } else if ((lang + "shortdescription").equals(key) && last.title == null) { 1396 last.title = value; 1397 } else if ("shortdescription".equals(key) && last.title == null) { 1398 last.title = value; 1399 } else if ((lang + "title").equals(key) && last.title == null) { 1400 last.title = value; 1401 } else if ("title".equals(key) && last.title == null) { 1402 last.title = value; 1403 } else if ("name".equals(key) && last.name == null) { 1404 last.name = value; 1405 } else if ((lang + "author").equals(key)) { 1406 last.author = value; 1407 } else if ((lang + "link").equals(key)) { 1408 last.link = value; 1409 } else if ((lang + "description").equals(key)) { 1410 last.description = value; 1411 } else if ("min-josm-version".equals(key)) { 1412 try { 1413 last.minJosmVersion = Integer.valueOf(value); 1414 } catch (NumberFormatException e) { 1415 // ignore 1416 if (Main.isTraceEnabled()) { 1417 Main.trace(e.getMessage()); 1418 } 1419 } 1420 } 1421 } 1422 } else { 1423 last = null; 1424 Matcher m = Pattern.compile("^(.+);(.+)$").matcher(line); 1425 if (m.matches()) { 1426 sources.add(last = new ExtendedSourceEntry(m.group(1), m.group(2))); 1427 } else { 1428 Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line)); 1429 } 1430 } 1431 } 1432 } catch (IOException e) { 1433 if (canceled) 1434 // ignore the exception and return 1435 return; 1436 OsmTransferException ex = new OsmTransferException(e); 1437 ex.setUrl(url); 1438 warn(ex); 1439 return; 1440 } 1441 } 1442 1443 @Override 1444 protected void finish() { 1445 Collections.sort(sources); 1446 availableSourcesModel.setSources(sources); 1447 } 1448 } 1449 1450 static class SourceEntryTableCellRenderer extends DefaultTableCellRenderer { 1451 @Override 1452 public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { 1453 if (value == null) 1454 return this; 1455 return super.getTableCellRendererComponent(table, 1456 fromSourceEntry((SourceEntry) value), isSelected, hasFocus, row, column); 1457 } 1458 1459 private static String fromSourceEntry(SourceEntry entry) { 1460 if (entry == null) 1461 return null; 1462 StringBuilder s = new StringBuilder("<html><b>"); 1463 if (entry.title != null) { 1464 s.append(entry.title).append("</b> <span color=\"gray\">"); 1465 } 1466 s.append(entry.url); 1467 if (entry.title != null) { 1468 s.append("</span>"); 1469 } 1470 s.append("</html>"); 1471 return s.toString(); 1472 } 1473 } 1474 1475 class FileOrUrlCellEditor extends JPanel implements TableCellEditor { 1476 private JosmTextField tfFileName; 1477 private final CopyOnWriteArrayList<CellEditorListener> listeners; 1478 private String value; 1479 private final boolean isFile; 1480 1481 /** 1482 * build the GUI 1483 */ 1484 protected final void build() { 1485 setLayout(new GridBagLayout()); 1486 GridBagConstraints gc = new GridBagConstraints(); 1487 gc.gridx = 0; 1488 gc.gridy = 0; 1489 gc.fill = GridBagConstraints.BOTH; 1490 gc.weightx = 1.0; 1491 gc.weighty = 1.0; 1492 add(tfFileName = new JosmTextField(), gc); 1493 1494 gc.gridx = 1; 1495 gc.gridy = 0; 1496 gc.fill = GridBagConstraints.BOTH; 1497 gc.weightx = 0.0; 1498 gc.weighty = 1.0; 1499 add(new JButton(new LaunchFileChooserAction())); 1500 1501 tfFileName.addFocusListener( 1502 new FocusAdapter() { 1503 @Override 1504 public void focusGained(FocusEvent e) { 1505 tfFileName.selectAll(); 1506 } 1507 } 1508 ); 1509 } 1510 1511 FileOrUrlCellEditor(boolean isFile) { 1512 this.isFile = isFile; 1513 listeners = new CopyOnWriteArrayList<>(); 1514 build(); 1515 } 1516 1517 @Override 1518 public void addCellEditorListener(CellEditorListener l) { 1519 if (l != null) { 1520 listeners.addIfAbsent(l); 1521 } 1522 } 1523 1524 protected void fireEditingCanceled() { 1525 for (CellEditorListener l: listeners) { 1526 l.editingCanceled(new ChangeEvent(this)); 1527 } 1528 } 1529 1530 protected void fireEditingStopped() { 1531 for (CellEditorListener l: listeners) { 1532 l.editingStopped(new ChangeEvent(this)); 1533 } 1534 } 1535 1536 @Override 1537 public void cancelCellEditing() { 1538 fireEditingCanceled(); 1539 } 1540 1541 @Override 1542 public Object getCellEditorValue() { 1543 return value; 1544 } 1545 1546 @Override 1547 public boolean isCellEditable(EventObject anEvent) { 1548 if (anEvent instanceof MouseEvent) 1549 return ((MouseEvent) anEvent).getClickCount() >= 2; 1550 return true; 1551 } 1552 1553 @Override 1554 public void removeCellEditorListener(CellEditorListener l) { 1555 listeners.remove(l); 1556 } 1557 1558 @Override 1559 public boolean shouldSelectCell(EventObject anEvent) { 1560 return true; 1561 } 1562 1563 @Override 1564 public boolean stopCellEditing() { 1565 value = tfFileName.getText(); 1566 fireEditingStopped(); 1567 return true; 1568 } 1569 1570 public void setInitialValue(String initialValue) { 1571 this.value = initialValue; 1572 if (initialValue == null) { 1573 this.tfFileName.setText(""); 1574 } else { 1575 this.tfFileName.setText(initialValue); 1576 } 1577 } 1578 1579 @Override 1580 public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 1581 setInitialValue((String) value); 1582 tfFileName.selectAll(); 1583 return this; 1584 } 1585 1586 class LaunchFileChooserAction extends AbstractAction { 1587 LaunchFileChooserAction() { 1588 putValue(NAME, "..."); 1589 putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file")); 1590 } 1591 1592 @Override 1593 public void actionPerformed(ActionEvent e) { 1594 FileChooserManager fcm = new FileChooserManager(true).createFileChooser(); 1595 if (!isFile) { 1596 fcm.getFileChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 1597 } 1598 prepareFileChooser(tfFileName.getText(), fcm.getFileChooser()); 1599 AbstractFileChooser fc = fcm.openFileChooser(JOptionPane.getFrameForComponent(SourceEditor.this)); 1600 if (fc != null) { 1601 tfFileName.setText(fc.getSelectedFile().toString()); 1602 } 1603 } 1604 } 1605 } 1606 1607 public abstract static class SourcePrefHelper { 1608 1609 private final String pref; 1610 1611 /** 1612 * Constructs a new {@code SourcePrefHelper} for the given preference key. 1613 * @param pref The preference key 1614 */ 1615 public SourcePrefHelper(String pref) { 1616 this.pref = pref; 1617 } 1618 1619 /** 1620 * Returns the default sources provided by JOSM core. 1621 * @return the default sources provided by JOSM core 1622 */ 1623 public abstract Collection<ExtendedSourceEntry> getDefault(); 1624 1625 public abstract Map<String, String> serialize(SourceEntry entry); 1626 1627 public abstract SourceEntry deserialize(Map<String, String> entryStr); 1628 1629 /** 1630 * Returns the list of sources. 1631 * @return The list of sources 1632 */ 1633 public List<SourceEntry> get() { 1634 1635 Collection<Map<String, String>> src = Main.pref.getListOfStructs(pref, (Collection<Map<String, String>>) null); 1636 if (src == null) 1637 return new ArrayList<SourceEntry>(getDefault()); 1638 1639 List<SourceEntry> entries = new ArrayList<>(); 1640 for (Map<String, String> sourcePref : src) { 1641 SourceEntry e = deserialize(new HashMap<>(sourcePref)); 1642 if (e != null) { 1643 entries.add(e); 1644 } 1645 } 1646 return entries; 1647 } 1648 1649 public boolean put(Collection<? extends SourceEntry> entries) { 1650 Collection<Map<String, String>> setting = new ArrayList<>(entries.size()); 1651 for (SourceEntry e : entries) { 1652 setting.add(serialize(e)); 1653 } 1654 return Main.pref.putListOfStructs(pref, setting); 1655 } 1656 1657 /** 1658 * Returns the set of active source URLs. 1659 * @return The set of active source URLs. 1660 */ 1661 public final Set<String> getActiveUrls() { 1662 Set<String> urls = new LinkedHashSet<>(); // retain order 1663 for (SourceEntry e : get()) { 1664 if (e.active) { 1665 urls.add(e.url); 1666 } 1667 } 1668 return urls; 1669 } 1670 } 1671 1672 /** 1673 * Defers loading of sources to the first time the adequate tab is selected. 1674 * @param tab The preferences tab 1675 * @param component The tab component 1676 * @since 6670 1677 */ 1678 public final void deferLoading(final DefaultTabPreferenceSetting tab, final Component component) { 1679 tab.getTabPane().addChangeListener( 1680 new ChangeListener() { 1681 @Override 1682 public void stateChanged(ChangeEvent e) { 1683 if (tab.getTabPane().getSelectedComponent() == component) { 1684 SourceEditor.this.initiallyLoadAvailableSources(); 1685 } 1686 } 1687 } 1688 ); 1689 } 1690 1691 protected String getTitleForSourceEntry(SourceEntry entry) { 1692 return "".equals(entry.title) ? null : entry.title; 1693 } 1694}