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 setEnabled(getSelectedColumnCount() >= 1 && getSelectedRowCount() >= 1); 223 } 224 } 225 226 /** 227 * Action to be run when the user adds a new tag. 228 * 229 */ 230 class AddAction extends AbstractAction implements PropertyChangeListener { 231 AddAction() { 232 new ImageProvider("dialogs", "add").getResource().attachImageIcon(this); 233 putValue(SHORT_DESCRIPTION, tr("Add a new tag")); 234 TagTable.this.addPropertyChangeListener(this); 235 updateEnabledState(); 236 } 237 238 @Override 239 public void actionPerformed(ActionEvent e) { 240 CellEditor cEditor = getCellEditor(); 241 if (cEditor != null) { 242 cEditor.stopCellEditing(); 243 } 244 final int rowIdx = model.getRowCount()-1; 245 if (rowIdx < 0 || !((TagModel) model.getValueAt(rowIdx, 0)).getName().trim().isEmpty()) { 246 model.appendNewTag(); 247 } 248 requestFocusInCell(model.getRowCount()-1, 0); 249 } 250 251 protected final void updateEnabledState() { 252 setEnabled(TagTable.this.isEnabled()); 253 } 254 255 @Override 256 public void propertyChange(PropertyChangeEvent evt) { 257 updateEnabledState(); 258 } 259 } 260 261 /** 262 * Action to be run when the user wants to paste tags from buffer 263 */ 264 class PasteAction extends AbstractAction implements PropertyChangeListener { 265 PasteAction() { 266 new ImageProvider("pastetags").getResource().attachImageIcon(this); 267 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer")); 268 TagTable.this.addPropertyChangeListener(this); 269 updateEnabledState(); 270 } 271 272 @Override 273 public void actionPerformed(ActionEvent e) { 274 Relation relation = new Relation(); 275 model.applyToPrimitive(relation); 276 new OsmTransferHandler().pasteTags(Collections.singleton(relation)); 277 model.updateTags(new TagMap(relation.getKeys()).getTags()); 278 } 279 280 protected final void updateEnabledState() { 281 setEnabled(TagTable.this.isEnabled()); 282 } 283 284 @Override 285 public void propertyChange(PropertyChangeEvent evt) { 286 updateEnabledState(); 287 } 288 } 289 290 /** the delete action */ 291 private DeleteAction deleteAction; 292 293 /** the add action */ 294 private AddAction addAction; 295 296 /** the tag paste action */ 297 private PasteAction pasteAction; 298 299 /** 300 * Returns the delete action. 301 * @return the delete action used by this table 302 */ 303 public DeleteAction getDeleteAction() { 304 return deleteAction; 305 } 306 307 /** 308 * Returns the add action. 309 * @return the add action used by this table 310 */ 311 public AddAction getAddAction() { 312 return addAction; 313 } 314 315 /** 316 * Returns the paste action. 317 * @return the paste action used by this table 318 */ 319 public PasteAction getPasteAction() { 320 return pasteAction; 321 } 322 323 /** 324 * initialize the table 325 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited 326 */ 327 protected final void init(final int maxCharacters) { 328 setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 329 setRowSelectionAllowed(true); 330 setColumnSelectionAllowed(true); 331 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); 332 333 // make ENTER behave like TAB 334 // 335 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 336 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell"); 337 338 // install custom navigation actions 339 // 340 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction()); 341 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction()); 342 343 // create a delete action. Installing this action in the input and action map 344 // didn't work. We therefore handle delete requests in processKeyBindings(...) 345 // 346 deleteAction = new DeleteAction(); 347 348 // create the add action 349 // 350 addAction = new AddAction(); 351 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) 352 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_DOWN_MASK), "addTag"); 353 getActionMap().put("addTag", addAction); 354 355 pasteAction = new PasteAction(); 356 357 // create the table cell editor and set it to key and value columns 358 // 359 TagCellEditor tmpEditor = new TagCellEditor(maxCharacters); 360 setRowHeight(tmpEditor.getEditor().getPreferredSize().height); 361 setTagCellEditor(tmpEditor); 362 } 363 364 /** 365 * Creates a new tag table 366 * 367 * @param model the tag editor model 368 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited 369 */ 370 public TagTable(TagEditorModel model, final int maxCharacters) { 371 super(model, new TagTableColumnModelBuilder(new TagCellRenderer(), tr("Key"), tr("Value")) 372 .setSelectionModel(model.getColumnSelectionModel()).build(), 373 model.getRowSelectionModel()); 374 this.model = model; 375 model.setEndEditListener(this); 376 init(maxCharacters); 377 } 378 379 @Override 380 public Dimension getPreferredSize() { 381 return getPreferredFullWidthSize(); 382 } 383 384 @Override 385 protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) { 386 387 // handle delete key 388 // 389 if (e.getKeyCode() == KeyEvent.VK_DELETE) { 390 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) 391 // if DEL was pressed and only the currently edited cell is selected, 392 // don't run the delete action. DEL is handled by the CellEditor as normal 393 // DEL in the text input. 394 // 395 return super.processKeyBinding(ks, e, condition, pressed); 396 getDeleteAction().actionPerformed(null); 397 } 398 return super.processKeyBinding(ks, e, condition, pressed); 399 } 400 401 /** 402 * Sets the editor autocompletion list 403 * @param autoCompletionList autocompletion list 404 */ 405 public void setAutoCompletionList(AutoCompletionList autoCompletionList) { 406 if (autoCompletionList == null) 407 return; 408 if (editor != null) { 409 editor.setAutoCompletionList(autoCompletionList); 410 } 411 } 412 413 /** 414 * Sets the autocompletion manager that should be used for editing the cells 415 * @param autocomplete The {@link AutoCompletionManager} 416 */ 417 public void setAutoCompletionManager(AutoCompletionManager autocomplete) { 418 if (autocomplete == null) { 419 Logging.warn("argument autocomplete should not be null. Aborting."); 420 Logging.error(new Exception()); 421 return; 422 } 423 if (editor != null) { 424 editor.setAutoCompletionManager(autocomplete); 425 } 426 } 427 428 /** 429 * Gets the {@link AutoCompletionList} the cell editor is synchronized with 430 * @return The list 431 */ 432 public AutoCompletionList getAutoCompletionList() { 433 if (editor != null) 434 return editor.getAutoCompletionList(); 435 else 436 return null; 437 } 438 439 /** 440 * Sets the next component to request focus after navigation (with tab or enter). 441 * @param nextFocusComponent next component to request focus after navigation (with tab or enter) 442 */ 443 public void setNextFocusComponent(Component nextFocusComponent) { 444 this.nextFocusComponent = nextFocusComponent; 445 } 446 447 /** 448 * Gets the editor that is used for the table cells 449 * @return The editor that is used when the user wants to enter text into a cell 450 */ 451 public TagCellEditor getTableCellEditor() { 452 return editor; 453 } 454 455 /** 456 * Inject a tag cell editor in the tag table 457 * 458 * @param editor tag cell editor 459 */ 460 public void setTagCellEditor(TagCellEditor editor) { 461 endCellEditing(); 462 this.editor = editor; 463 getColumnModel().getColumn(0).setCellEditor(editor); 464 getColumnModel().getColumn(1).setCellEditor(editor); 465 } 466 467 /** 468 * Request the focus in a specific cell 469 * @param row The row index 470 * @param col The column index 471 */ 472 public void requestFocusInCell(final int row, final int col) { 473 changeSelection(row, col, false, false); 474 editCellAt(row, col); 475 Component c = getEditorComponent(); 476 if (c != null) { 477 if (!c.requestFocusInWindow()) { 478 Logging.warn("Unable to request focus for " + c); 479 } 480 if (c instanceof JTextComponent) { 481 ((JTextComponent) c).selectAll(); 482 } 483 } 484 // there was a bug here - on older 1.6 Java versions Tab was not working 485 // after such activation. In 1.7 it works OK, 486 // previous solution of using awt.Robot was resetting mouse speed on Windows 487 } 488 489 /** 490 * Marks a component that may be focused without stopping the cell editing 491 * @param component The component 492 */ 493 public void addComponentNotStoppingCellEditing(Component component) { 494 if (component == null) return; 495 doNotStopCellEditingWhenFocused.addIfAbsent(component); 496 } 497 498 /** 499 * Removes a component added with {@link #addComponentNotStoppingCellEditing(Component)} 500 * @param component The component 501 */ 502 public void removeComponentNotStoppingCellEditing(Component component) { 503 if (component == null) return; 504 doNotStopCellEditingWhenFocused.remove(component); 505 } 506 507 @Override 508 public boolean editCellAt(int row, int column, EventObject e) { 509 510 // a snipped copied from the Java 1.5 implementation of JTable 511 // 512 if (cellEditor != null && !cellEditor.stopCellEditing()) 513 return false; 514 515 if (row < 0 || row >= getRowCount() || 516 column < 0 || column >= getColumnCount()) 517 return false; 518 519 if (!isCellEditable(row, column)) 520 return false; 521 522 // make sure our custom implementation of CellEditorRemover is created 523 if (editorRemover == null) { 524 KeyboardFocusManager fm = 525 KeyboardFocusManager.getCurrentKeyboardFocusManager(); 526 editorRemover = new CellEditorRemover(fm); 527 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover); 528 } 529 530 // delegate to the default implementation 531 return super.editCellAt(row, column, e); 532 } 533 534 @Override 535 public void endCellEditing() { 536 if (isEditing()) { 537 CellEditor cEditor = getCellEditor(); 538 if (cEditor != null) { 539 // First attempt to commit. If this does not work, cancel. 540 cEditor.stopCellEditing(); 541 cEditor.cancelCellEditing(); 542 } 543 } 544 } 545 546 @Override 547 public void removeEditor() { 548 // make sure we unregister our custom implementation of CellEditorRemover 549 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 550 removePropertyChangeListener("permanentFocusOwner", editorRemover); 551 editorRemover = null; 552 super.removeEditor(); 553 } 554 555 @Override 556 public void removeNotify() { 557 // make sure we unregister our custom implementation of CellEditorRemover 558 KeyboardFocusManager.getCurrentKeyboardFocusManager(). 559 removePropertyChangeListener("permanentFocusOwner", editorRemover); 560 editorRemover = null; 561 super.removeNotify(); 562 } 563 564 /** 565 * This is a custom implementation of the CellEditorRemover used in JTable 566 * to handle the client property <code>terminateEditOnFocusLost</code>. 567 * 568 * This implementation also checks whether focus is transferred to one of a list 569 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}. 570 * A typical example for such a component is a button in {@link TagEditorPanel} 571 * which isn't a child component of {@link TagTable} but which should respond to 572 * to focus transfer in a similar way to a child of TagTable. 573 * 574 */ 575 class CellEditorRemover implements PropertyChangeListener { 576 private final KeyboardFocusManager focusManager; 577 578 CellEditorRemover(KeyboardFocusManager fm) { 579 this.focusManager = fm; 580 } 581 582 @Override 583 public void propertyChange(PropertyChangeEvent ev) { 584 if (!isEditing()) 585 return; 586 587 Component c = focusManager.getPermanentFocusOwner(); 588 while (c != null) { 589 if (c == TagTable.this) 590 // focus remains inside the table 591 return; 592 if (doNotStopCellEditingWhenFocused.contains(c)) 593 // focus remains on one of the associated components 594 return; 595 else if (c instanceof Window) { 596 if (c == SwingUtilities.getRoot(TagTable.this) && !getCellEditor().stopCellEditing()) { 597 getCellEditor().cancelCellEditing(); 598 } 599 break; 600 } 601 c = c.getParent(); 602 } 603 } 604 } 605}