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