001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.applet.Applet; 007import java.awt.Component; 008import java.awt.Container; 009import java.awt.Dimension; 010import java.awt.KeyboardFocusManager; 011import java.awt.Window; 012import java.awt.event.ActionEvent; 013import java.awt.event.KeyEvent; 014import java.awt.event.KeyListener; 015import java.beans.PropertyChangeEvent; 016import java.beans.PropertyChangeListener; 017import java.util.ArrayList; 018import java.util.Collections; 019import java.util.EventObject; 020import java.util.List; 021import java.util.Map; 022import java.util.concurrent.CopyOnWriteArrayList; 023 024import javax.swing.AbstractAction; 025import javax.swing.CellEditor; 026import javax.swing.DefaultListSelectionModel; 027import javax.swing.JComponent; 028import javax.swing.JTable; 029import javax.swing.JViewport; 030import javax.swing.KeyStroke; 031import javax.swing.ListSelectionModel; 032import javax.swing.SwingUtilities; 033import javax.swing.event.ListSelectionEvent; 034import javax.swing.event.ListSelectionListener; 035import javax.swing.table.DefaultTableColumnModel; 036import javax.swing.table.TableColumn; 037import javax.swing.text.JTextComponent; 038 039import org.openstreetmap.josm.Main; 040import org.openstreetmap.josm.actions.CopyAction; 041import org.openstreetmap.josm.actions.PasteTagsAction; 042import org.openstreetmap.josm.data.osm.OsmPrimitive; 043import org.openstreetmap.josm.data.osm.PrimitiveData; 044import org.openstreetmap.josm.data.osm.Relation; 045import org.openstreetmap.josm.data.osm.Tag; 046import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction; 047import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 048import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; 049import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 050import org.openstreetmap.josm.tools.ImageProvider; 051import org.openstreetmap.josm.tools.TextTagParser; 052import org.openstreetmap.josm.tools.Utils; 053 054/** 055 * This is the tabular editor component for OSM tags. 056 * 057 */ 058public class TagTable extends JTable { 059 /** the table cell editor used by this table */ 060 private TagCellEditor editor = null; 061 private final TagEditorModel model; 062 private Component nextFocusComponent; 063 064 /** a list of components to which focus can be transferred without stopping 065 * cell editing this table. 066 */ 067 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>(); 068 private CellEditorRemover editorRemover; 069 070 /** 071 * The table has two columns. The first column is used for editing rendering and 072 * editing tag keys, the second for rendering and editing tag values. 073 * 074 */ 075 static class TagTableColumnModel extends DefaultTableColumnModel { 076 public TagTableColumnModel(DefaultListSelectionModel selectionModel) { 077 setSelectionModel(selectionModel); 078 TableColumn col = null; 079 TagCellRenderer renderer = new TagCellRenderer(); 080 081 // column 0 - tag key 082 col = new TableColumn(0); 083 col.setHeaderValue(tr("Key")); 084 col.setResizable(true); 085 col.setCellRenderer(renderer); 086 addColumn(col); 087 088 // column 1 - tag value 089 col = new TableColumn(1); 090 col.setHeaderValue(tr("Value")); 091 col.setResizable(true); 092 col.setCellRenderer(renderer); 093 addColumn(col); 094 } 095 } 096 097 /** 098 * Action to be run when the user navigates to the next cell in the table, 099 * for instance by pressing TAB or ENTER. The action alters the standard 100 * navigation path from cell to cell: 101 * <ul> 102 * <li>it jumps over cells in the first column</li> 103 * <li>it automatically add a new empty row when the user leaves the 104 * last cell in the table</li> 105 * <ul> 106 * 107 */ 108 class SelectNextColumnCellAction extends AbstractAction { 109 @Override 110 public void actionPerformed(ActionEvent e) { 111 run(); 112 } 113 114 public void run() { 115 int col = getSelectedColumn(); 116 int row = getSelectedRow(); 117 if (getCellEditor() != null) { 118 getCellEditor().stopCellEditing(); 119 } 120 121 if (row==-1 && col==-1) { 122 requestFocusInCell(0, 0); 123 return; 124 } 125 126 if (col == 0) { 127 col++; 128 } else if (col == 1 && row < getRowCount()-1) { 129 col=0; 130 row++; 131 } else if (col == 1 && row == getRowCount()-1){ 132 // we are at the end. Append an empty row and move the focus 133 // to its second column 134 String key = ((TagModel)model.getValueAt(row, 0)).getName(); 135 if (!key.trim().isEmpty()) { 136 model.appendNewTag(); 137 col=0; 138 row++; 139 } else { 140 clearSelection(); 141 if (nextFocusComponent!=null) 142 nextFocusComponent.requestFocusInWindow(); 143 return; 144 } 145 } 146 requestFocusInCell(row,col); 147 } 148 } 149 150 /** 151 * Action to be run when the user navigates to the previous cell in the table, 152 * for instance by pressing Shift-TAB 153 * 154 */ 155 class SelectPreviousColumnCellAction extends AbstractAction { 156 157 @Override 158 public void actionPerformed(ActionEvent e) { 159 int col = getSelectedColumn(); 160 int row = getSelectedRow(); 161 if (getCellEditor() != null) { 162 getCellEditor().stopCellEditing(); 163 } 164 165 if (col <= 0 && row <= 0) { 166 // change nothing 167 } else if (col == 1) { 168 col--; 169 } else { 170 col = 1; 171 row--; 172 } 173 requestFocusInCell(row,col); 174 } 175 } 176 177 /** 178 * Action to be run when the user invokes a delete action on the table, for 179 * instance by pressing DEL. 180 * 181 * Depending on the shape on the current selection the action deletes individual 182 * values or entire tags from the model. 183 * 184 * If the current selection consists of cells in the second column only, the keys of 185 * the selected tags are set to the empty string. 186 * 187 * If the current selection consists of cell in the third column only, the values of the 188 * selected tags are set to the empty string. 189 * 190 * If the current selection consists of cells in the second and the third column, 191 * the selected tags are removed from the model. 192 * 193 * This action listens to the table selection. It becomes enabled when the selection 194 * is non-empty, otherwise it is disabled. 195 * 196 * 197 */ 198 class DeleteAction extends RunnableAction implements ListSelectionListener { 199 200 public DeleteAction() { 201 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete")); 202 putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table")); 203 getSelectionModel().addListSelectionListener(this); 204 getColumnModel().getSelectionModel().addListSelectionListener(this); 205 updateEnabledState(); 206 } 207 208 /** 209 * delete a selection of tag names 210 */ 211 protected void deleteTagNames() { 212 int[] rows = getSelectedRows(); 213 model.deleteTagNames(rows); 214 } 215 216 /** 217 * delete a selection of tag values 218 */ 219 protected void deleteTagValues() { 220 int[] rows = getSelectedRows(); 221 model.deleteTagValues(rows); 222 } 223 224 /** 225 * delete a selection of tags 226 */ 227 protected void deleteTags() { 228 int[] rows = getSelectedRows(); 229 model.deleteTags(rows); 230 } 231 232 @Override 233 public void run() { 234 if (!isEnabled()) 235 return; 236 switch(getSelectedColumnCount()) { 237 case 1: 238 if (getSelectedColumn() == 0) { 239 deleteTagNames(); 240 } else if (getSelectedColumn() == 1) { 241 deleteTagValues(); 242 } 243 break; 244 case 2: 245 deleteTags(); 246 break; 247 } 248 249 if (isEditing()) { 250 CellEditor editor = getCellEditor(); 251 if (editor != null) { 252 editor.cancelCellEditing(); 253 } 254 } 255 256 if (model.getRowCount() == 0) { 257 model.ensureOneTag(); 258 requestFocusInCell(0, 0); 259 } 260 } 261 262 /** 263 * listens to the table selection model 264 */ 265 @Override 266 public void valueChanged(ListSelectionEvent e) { 267 updateEnabledState(); 268 } 269 270 protected void updateEnabledState() { 271 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) { 272 setEnabled(true); 273 } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) { 274 setEnabled(true); 275 } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) { 276 setEnabled(true); 277 } else { 278 setEnabled(false); 279 } 280 } 281 } 282 283 /** 284 * Action to be run when the user adds a new tag. 285 * 286 * 287 */ 288 class AddAction extends RunnableAction implements PropertyChangeListener{ 289 public AddAction() { 290 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add")); 291 putValue(SHORT_DESCRIPTION, tr("Add a new tag")); 292 TagTable.this.addPropertyChangeListener(this); 293 updateEnabledState(); 294 } 295 296 @Override 297 public void run() { 298 CellEditor editor = getCellEditor(); 299 if (editor != null) { 300 getCellEditor().stopCellEditing(); 301 } 302 final int rowIdx = model.getRowCount()-1; 303 String key = ((TagModel)model.getValueAt(rowIdx, 0)).getName(); 304 if (!key.trim().isEmpty()) { 305 model.appendNewTag(); 306 } 307 requestFocusInCell(model.getRowCount()-1, 0); 308 } 309 310 protected void updateEnabledState() { 311 setEnabled(TagTable.this.isEnabled()); 312 } 313 314 @Override 315 public void propertyChange(PropertyChangeEvent evt) { 316 updateEnabledState(); 317 } 318 } 319 320 /** 321 * Action to be run when the user wants to paste tags from buffer 322 */ 323 class PasteAction extends RunnableAction implements PropertyChangeListener{ 324 public PasteAction() { 325 putValue(SMALL_ICON, ImageProvider.get("","pastetags")); 326 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer")); 327 TagTable.this.addPropertyChangeListener(this); 328 updateEnabledState(); 329 } 330 331 @Override 332 public void run() { 333 Relation relation = new Relation(); 334 model.applyToPrimitive(relation); 335 336 String buf = Utils.getClipboardContent(); 337 if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) { 338 List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded(); 339 if (directlyAdded==null || directlyAdded.isEmpty()) return; 340 PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, Collections.<OsmPrimitive>singletonList(relation)); 341 model.updateTags(tagPaster.execute()); 342 } else { 343 // Paste tags from arbitrary text 344 Map<String, String> tags = TextTagParser.readTagsFromText(buf); 345 if (tags==null || tags.isEmpty()) { 346 TextTagParser.showBadBufferMessage(ht("/Action/PasteTags")); 347 } else if (TextTagParser.validateTags(tags)) { 348 List<Tag> newTags = new ArrayList<Tag>(); 349 for (Map.Entry<String, String> entry: tags.entrySet()) { 350 String k = entry.getKey(); 351 String v = entry.getValue(); 352 newTags.add(new Tag(k,v)); 353 } 354 model.updateTags(newTags); 355 } 356 } 357 } 358 359 protected void updateEnabledState() { 360 setEnabled(TagTable.this.isEnabled()); 361 } 362 363 @Override 364 public void propertyChange(PropertyChangeEvent evt) { 365 updateEnabledState(); 366 } 367 } 368 369 /** the delete action */ 370 private RunnableAction deleteAction = null; 371 372 /** the add action */ 373 private RunnableAction addAction = null; 374 375 /** the tag paste action */ 376 private RunnableAction pasteAction = null; 377 378 /** 379 * 380 * @return the delete action used by this table 381 */ 382 public RunnableAction getDeleteAction() { 383 return deleteAction; 384 } 385 386 public RunnableAction getAddAction() { 387 return addAction; 388 } 389 390 public RunnableAction getPasteAction() { 391 return pasteAction; 392 } 393 394 /** 395 * initialize the table 396 */ 397 protected void init() { 398 setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 399 setRowSelectionAllowed(true); 400 setColumnSelectionAllowed(true); 401 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); 402 403 // make ENTER behave like TAB 404 // 405 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 406 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell"); 407 408 // install custom navigation actions 409 // 410 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction()); 411 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction()); 412 413 // create a delete action. Installing this action in the input and action map 414 // didn't work. We therefore handle delete requests in processKeyBindings(...) 415 // 416 deleteAction = new DeleteAction(); 417 418 // create the add action 419 // 420 addAction = new AddAction(); 421 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 422 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag"); 423 getActionMap().put("addTag", addAction); 424 425 pasteAction = new PasteAction(); 426 427 // create the table cell editor and set it to key and value columns 428 // 429 TagCellEditor tmpEditor = new TagCellEditor(); 430 setRowHeight(tmpEditor.getEditor().getPreferredSize().height); 431 setTagCellEditor(tmpEditor); 432 } 433 434 /** 435 * Creates a new tag table 436 * 437 * @param model the tag editor model 438 */ 439 public TagTable(TagEditorModel model) { 440 super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel()); 441 this.model = model; 442 init(); 443 } 444 445 @Override 446 public Dimension getPreferredSize(){ 447 Container c = getParent(); 448 while(c != null && ! (c instanceof JViewport)) { 449 c = c.getParent(); 450 } 451 if (c != null) { 452 Dimension d = super.getPreferredSize(); 453 d.width = c.getSize().width; 454 return d; 455 } 456 return super.getPreferredSize(); 457 } 458 459 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, 460 int condition, boolean pressed) { 461 462 // handle delete key 463 // 464 if (e.getKeyCode() == KeyEvent.VK_DELETE) { 465 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) 466 // if DEL was pressed and only the currently edited cell is selected, 467 // don't run the delete action. DEL is handled by the CellEditor as normal 468 // DEL in the text input. 469 // 470 return super.processKeyBinding(ks, e, condition, pressed); 471 getDeleteAction().run(); 472 } 473 return super.processKeyBinding(ks, e, condition, pressed); 474 } 475 476 /** 477 * @param autoCompletionList 478 */ 479 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 480 if (autoCompletionList == null) 481 return; 482 if (editor != null) { 483 editor.setAutoCompletionList(autoCompletionList); 484 } 485 } 486 487 public void setAutoCompletionManager(AutoCompletionManager autocomplete) { 488 if (autocomplete == null) { 489 Main.warn("argument autocomplete should not be null. Aborting."); 490 Thread.dumpStack(); 491 return; 492 } 493 if (editor != null) { 494 editor.setAutoCompletionManager(autocomplete); 495 } 496 } 497 498 public AutoCompletionList getAutoCompletionList() { 499 if (editor != null) 500 return editor.getAutoCompletionList(); 501 else 502 return null; 503 } 504 505 public void setNextFocusComponent(Component nextFocusComponent) { 506 this.nextFocusComponent = nextFocusComponent; 507 } 508 509 public TagCellEditor getTableCellEditor() { 510 return editor; 511 } 512 513 public void addOKAccelatorListener(KeyListener l) { 514 addKeyListener(l); 515 if (editor != null) { 516 editor.getEditor().addKeyListener(l); 517 } 518 } 519 520 /** 521 * Inject a tag cell editor in the tag table 522 * 523 * @param editor 524 */ 525 public void setTagCellEditor(TagCellEditor editor) { 526 if (isEditing()) { 527 this.editor.cancelCellEditing(); 528 } 529 this.editor = editor; 530 getColumnModel().getColumn(0).setCellEditor(editor); 531 getColumnModel().getColumn(1).setCellEditor(editor); 532 } 533 534 public void requestFocusInCell(final int row, final int col) { 535 changeSelection(row, col, false, false); 536 editCellAt(row, col); 537 Component c = getEditorComponent(); 538 if (c!=null) { 539 c.requestFocusInWindow(); 540 if ( c instanceof JTextComponent ) { 541 ( (JTextComponent)c ).selectAll(); 542 } 543 } 544 // there was a bug here - on older 1.6 Java versions Tab was not working 545 // after such activation. In 1.7 it works OK, 546 // previous solution of usint awt.Robot was resetting mouse speed on Windows 547 } 548 549 public void addComponentNotStoppingCellEditing(Component component) { 550 if (component == null) return; 551 doNotStopCellEditingWhenFocused.addIfAbsent(component); 552 } 553 554 public void removeComponentNotStoppingCellEditing(Component component) { 555 if (component == null) return; 556 doNotStopCellEditingWhenFocused.remove(component); 557 } 558 559 @Override 560 public boolean editCellAt(int row, int column, EventObject e){ 561 562 // a snipped copied from the Java 1.5 implementation of JTable 563 // 564 if (cellEditor != null && !cellEditor.stopCellEditing()) 565 return false; 566 567 if (row < 0 || row >= getRowCount() || 568 column < 0 || column >= getColumnCount()) 569 return false; 570 571 if (!isCellEditable(row, column)) 572 return false; 573 574 // make sure our custom implementation of CellEditorRemover is created 575 if (editorRemover == null) { 576 KeyboardFocusManager fm = 577 KeyboardFocusManager.getCurrentKeyboardFocusManager(); 578 editorRemover = new CellEditorRemover(fm); 579 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover); 580 } 581 582 // delegate to the default implementation 583 return super.editCellAt(row, column,e); 584 } 585 586 587 @Override 588 public void removeEditor() { 589 // make sure we unregister our custom implementation of CellEditorRemover 590 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 591 removePropertyChangeListener("permanentFocusOwner", editorRemover); 592 editorRemover = null; 593 super.removeEditor(); 594 } 595 596 @Override 597 public void removeNotify() { 598 // make sure we unregister our custom implementation of CellEditorRemover 599 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 600 removePropertyChangeListener("permanentFocusOwner", editorRemover); 601 editorRemover = null; 602 super.removeNotify(); 603 } 604 605 /** 606 * This is a custom implementation of the CellEditorRemover used in JTable 607 * to handle the client property <tt>terminateEditOnFocusLost</tt>. 608 * 609 * This implementation also checks whether focus is transferred to one of a list 610 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}. 611 * A typical example for such a component is a button in {@link TagEditorPanel} 612 * which isn't a child component of {@link TagTable} but which should respond to 613 * to focus transfer in a similar way to a child of TagTable. 614 * 615 */ 616 class CellEditorRemover implements PropertyChangeListener { 617 KeyboardFocusManager focusManager; 618 619 public CellEditorRemover(KeyboardFocusManager fm) { 620 this.focusManager = fm; 621 } 622 623 @Override 624 public void propertyChange(PropertyChangeEvent ev) { 625 if (!isEditing()) 626 return; 627 628 Component c = focusManager.getPermanentFocusOwner(); 629 while (c != null) { 630 if (c == TagTable.this) 631 // focus remains inside the table 632 return; 633 if (doNotStopCellEditingWhenFocused.contains(c)) 634 // focus remains on one of the associated components 635 return; 636 else if ((c instanceof Window) || 637 (c instanceof Applet && c.getParent() == null)) { 638 if (c == SwingUtilities.getRoot(TagTable.this)) { 639 if (!getCellEditor().stopCellEditing()) { 640 getCellEditor().cancelCellEditing(); 641 } 642 } 643 break; 644 } 645 c = c.getParent(); 646 } 647 } 648 } 649}