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