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