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