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