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