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