source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TagTable.java@ 2997

Last change on this file since 2997 was 2997, checked in by Gubaer, 14 years ago

fixed #4506: relation-editor: tag-delete-button does not work

File size: 19.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.applet.Applet;
7import java.awt.AWTException;
8import java.awt.Component;
9import java.awt.Container;
10import java.awt.Dimension;
11import java.awt.KeyboardFocusManager;
12import java.awt.MouseInfo;
13import java.awt.Point;
14import java.awt.Rectangle;
15import java.awt.Robot;
16import java.awt.Window;
17import java.awt.event.ActionEvent;
18import java.awt.event.InputEvent;
19import java.awt.event.KeyEvent;
20import java.awt.event.KeyListener;
21import java.beans.PropertyChangeEvent;
22import java.beans.PropertyChangeListener;
23import java.util.EventObject;
24import java.util.concurrent.CopyOnWriteArrayList;
25import java.util.logging.Level;
26import java.util.logging.Logger;
27
28import javax.swing.AbstractAction;
29import javax.swing.CellEditor;
30import javax.swing.DefaultListSelectionModel;
31import javax.swing.JComponent;
32import javax.swing.JTable;
33import javax.swing.JViewport;
34import javax.swing.KeyStroke;
35import javax.swing.ListSelectionModel;
36import javax.swing.SwingUtilities;
37import javax.swing.event.ListSelectionEvent;
38import javax.swing.event.ListSelectionListener;
39import javax.swing.table.DefaultTableColumnModel;
40import javax.swing.table.TableColumn;
41import javax.swing.table.TableModel;
42
43import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction;
44import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionCache;
45import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
46import org.openstreetmap.josm.tools.ImageProvider;
47
48/**
49 * This is the tabular editor component for OSM tags.
50 *
51 */
52public class TagTable extends JTable {
53
54 private static final Logger logger = Logger.getLogger(TagTable.class.getName());
55
56 /** the table cell editor used by this table */
57 private TagCellEditor editor = null;
58
59 /** a list of components to which focus can be transferred withouth stopping
60 * cell editing this table.
61 */
62 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>();
63 private CellEditorRemover editorRemover;
64
65 /**
66 * The table has two columns. The first column is used for editing rendering and
67 * editing tag keys, the second for rendering and editing tag values.
68 *
69 */
70 static class TagTableColumnModel extends DefaultTableColumnModel {
71 public TagTableColumnModel(DefaultListSelectionModel selectionModel) {
72 setSelectionModel(selectionModel);
73 TableColumn col = null;
74 TagCellRenderer renderer = new TagCellRenderer();
75
76 // column 0 - tag key
77 col = new TableColumn(0);
78 col.setHeaderValue(tr("Key"));
79 col.setResizable(true);
80 col.setCellRenderer(renderer);
81 addColumn(col);
82
83 // column 1 - tag value
84 col = new TableColumn(1);
85 col.setHeaderValue(tr("Value"));
86 col.setResizable(true);
87 col.setCellRenderer(renderer);
88 addColumn(col);
89 }
90 }
91
92 /**
93 * Action to be run when the user navigates to the next cell in the table,
94 * for instance by pressing TAB or ENTER. The action alters the standard
95 * navigation path from cell to cell:
96 * <ul>
97 * <li>it jumps over cells in the first column</li>
98 * <li>it automatically add a new empty row when the user leaves the
99 * last cell in the table</li>
100 * <ul>
101 *
102 */
103 class SelectNextColumnCellAction extends AbstractAction {
104 public void actionPerformed(ActionEvent e) {
105 run();
106 }
107
108 public void run() {
109 int col = getSelectedColumn();
110 int row = getSelectedRow();
111 if (getCellEditor() != null) {
112 getCellEditor().stopCellEditing();
113 }
114
115 if (col == 0) {
116 col++;
117 } else if (col == 1 && row < getRowCount()-1) {
118 col=0;
119 row++;
120 } else if (col == 1 && row == getRowCount()-1){
121 // we are at the end. Append an empty row and move the focus
122 // to its second column
123 TagEditorModel model = (TagEditorModel)getModel();
124 model.appendNewTag();
125 col=0;
126 row++;
127 }
128 changeSelection(row, col, false, false);
129 }
130 }
131
132 /**
133 * Action to be run when the user navigates to the previous cell in the table,
134 * for instance by pressing Shift-TAB
135 *
136 */
137 class SelectPreviousColumnCellAction extends AbstractAction {
138
139 public void actionPerformed(ActionEvent e) {
140 int col = getSelectedColumn();
141 int row = getSelectedRow();
142 if (getCellEditor() != null) {
143 getCellEditor().stopCellEditing();
144 }
145
146 if (col <= 0 && row <= 0) {
147 // change nothing
148 } else if (col == 1) {
149 col--;
150 } else {
151 col = 1;
152 row--;
153 }
154 changeSelection(row, col, false, false);
155 }
156 }
157
158 /**
159 * Action to be run when the user invokes a delete action on the table, for
160 * instance by pressing DEL.
161 *
162 * Depending on the shape on the current selection the action deletes individual
163 * values or entire tags from the model.
164 *
165 * If the current selection consists of cells in the second column only, the keys of
166 * the selected tags are set to the empty string.
167 *
168 * If the current selection consists of cell in the third column only, the values of the
169 * selected tags are set to the empty string.
170 *
171 * If the current selection consists of cells in the second and the third column,
172 * the selected tags are removed from the model.
173 *
174 * This action listens to the table selection. It becomes enabled when the selection
175 * is non-empty, otherwise it is disabled.
176 *
177 *
178 */
179 class DeleteAction extends RunnableAction implements ListSelectionListener {
180
181 public DeleteAction() {
182 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
183 putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
184 getSelectionModel().addListSelectionListener(this);
185 getColumnModel().getSelectionModel().addListSelectionListener(this);
186 updateEnabledState();
187 }
188
189 /**
190 * delete a selection of tag names
191 */
192 protected void deleteTagNames() {
193 int[] rows = getSelectedRows();
194 TagEditorModel model = (TagEditorModel)getModel();
195 model.deleteTagNames(rows);
196 }
197
198 /**
199 * delete a selection of tag values
200 */
201 protected void deleteTagValues() {
202 int[] rows = getSelectedRows();
203 TagEditorModel model = (TagEditorModel)getModel();
204 model.deleteTagValues(rows);
205 }
206
207 /**
208 * delete a selection of tags
209 */
210 protected void deleteTags() {
211 int[] rows = getSelectedRows();
212 TagEditorModel model = (TagEditorModel)getModel();
213 model.deleteTags(rows);
214 }
215
216 @Override
217 public void run() {
218 if (!isEnabled())
219 return;
220 switch(getSelectedColumnCount()) {
221 case 1:
222 if (isEditing()) {
223 getCellEditor().cancelCellEditing();
224 deleteTags();
225 } else if (getSelectedColumn() == 0) {
226 deleteTagNames();
227 } else if (getSelectedColumn() == 1) {
228 deleteTagValues();
229 } else
230 // should not happen
231 //
232 throw new IllegalStateException("unexpected selected column: getSelectedColumn() is " + getSelectedColumn());
233 break;
234 case 2:
235 deleteTags();
236 break;
237 }
238
239 if (isEditing()) {
240 CellEditor editor = getCellEditor();
241 if (editor != null) {
242 editor.cancelCellEditing();
243 }
244 }
245
246 TagEditorModel model = (TagEditorModel)getModel();
247 if (model.getRowCount() == 0) {
248 model.ensureOneTag();
249 requestFocusInCell(0, 0);
250 }
251 }
252
253 /**
254 * listens to the table selection model
255 */
256 public void valueChanged(ListSelectionEvent e) {
257 updateEnabledState();
258 }
259
260 protected void updateEnabledState() {
261 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
262 setEnabled(true);
263 } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
264 setEnabled(true);
265 } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
266 setEnabled(true);
267 } else {
268 setEnabled(false);
269 }
270 }
271 }
272
273 /**
274 * Action to be run when the user adds a new tag.
275 *
276 *
277 */
278 class AddAction extends RunnableAction implements PropertyChangeListener{
279 public AddAction() {
280 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
281 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
282 TagTable.this.addPropertyChangeListener(this);
283 updateEnabledState();
284 }
285
286 @Override
287 public void run() {
288 CellEditor editor = getCellEditor();
289 if (editor != null) {
290 getCellEditor().stopCellEditing();
291 }
292 ((TagEditorModel)getModel()).appendNewTag();
293 final int rowIdx = getModel().getRowCount()-1;
294 requestFocusInCell(rowIdx, 0);
295 }
296
297 protected void updateEnabledState() {
298 setEnabled(TagTable.this.isEnabled());
299 }
300
301 public void propertyChange(PropertyChangeEvent evt) {
302 updateEnabledState();
303 }
304 }
305
306 /** the delete action */
307 private RunnableAction deleteAction = null;
308
309 /** the add action */
310 private RunnableAction addAction = null;
311
312 /**
313 *
314 * @return the delete action used by this table
315 */
316 public RunnableAction getDeleteAction() {
317 return deleteAction;
318 }
319
320 public RunnableAction getAddAction() {
321 return addAction;
322 }
323
324 /**
325 * initialize the table
326 */
327 protected void init() {
328 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
329 //setCellSelectionEnabled(true);
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_MASK), "addTag");
354 getActionMap().put("addTag", addAction);
355
356 // create the table cell editor and set it to key and value columns
357 //
358 editor = new TagCellEditor();
359 getColumnModel().getColumn(0).setCellEditor(editor);
360 getColumnModel().getColumn(1).setCellEditor(editor);
361
362 getSelectionModel().addListSelectionListener(new ListSelectionListener() {
363
364 public void valueChanged(ListSelectionEvent e) {
365 ListSelectionModel rm = getSelectionModel();
366 ListSelectionModel cm = getColumnModel().getSelectionModel();
367 }
368 });
369 }
370
371 /**
372 * constructor
373 *
374 * @param model
375 * @param columnModel
376 */
377 public TagTable(TableModel model, DefaultListSelectionModel rowSelectionModel, DefaultListSelectionModel colSelectionModel) {
378 super(model, new TagTableColumnModel(colSelectionModel), rowSelectionModel);
379 init();
380 }
381
382 @Override
383 public Dimension getPreferredSize(){
384 Container c = getParent();
385 while(c != null && ! (c instanceof JViewport)) {
386 c = c.getParent();
387 }
388 if (c != null) {
389 Dimension d = super.getPreferredSize();
390 d.width = c.getSize().width;
391 return d;
392 }
393 return super.getPreferredSize();
394 }
395
396 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
397 int condition, boolean pressed) {
398
399 // handle delete key
400 //
401 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
402 getDeleteAction().run();
403 }
404 return super.processKeyBinding(ks, e, condition, pressed);
405 }
406
407 /**
408 * @param autoCompletionList
409 */
410 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
411 if (autoCompletionList == null)
412 return;
413 if (editor != null) {
414 editor.setAutoCompletionList(autoCompletionList);
415 }
416 }
417
418 public void setAutoCompletionCache(AutoCompletionCache acCache) {
419 if (acCache == null) {
420 logger.warning("argument acCache should not be null. Aborting.");
421 return;
422 }
423 if (editor != null) {
424 editor.setAutoCompletionCache(acCache);
425 }
426 }
427
428 public AutoCompletionList getAutoCompletionList() {
429 if (editor != null)
430 return editor.getAutoCompletionList();
431 else
432 return null;
433 }
434
435 public TagCellEditor getTableCellEditor() {
436 return editor;
437 }
438
439 public void addOKAccelatorListener(KeyListener l) {
440 addKeyListener(l);
441 if (editor == null) {
442 logger.warning("editor is null. cannot register OK accelator listener.");
443 } else {
444 editor.getEditor().addKeyListener(l);
445 }
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 logger.log(Level.SEVERE, "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 {@see TagTable#doNotStopCellEditingWhenFocused}.
543 * A typical example for such a component is a button in {@see TagEditorPanel}
544 * which isn't a child component of {@see 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}
Note: See TracBrowser for help on using the repository browser.