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