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