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