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