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