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