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