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

Last change on this file since 3214 was 3214, checked in by bastiK, 14 years ago

autocompletion cleanup - fixes #2729

  • Property svn:eol-style set to native
File size: 19.5 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;
41
42import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction;
43import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
44import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
45import org.openstreetmap.josm.tools.ImageProvider;
46
47/**
48 * This is the tabular editor component for OSM tags.
49 *
50 */
51public class TagTable extends JTable {
52
53 private static final Logger logger = Logger.getLogger(TagTable.class.getName());
54
55 /** the table cell editor used by this table */
56 private TagCellEditor editor = null;
57
58 /** a list of components to which focus can be transferred withouth stopping
59 * cell editing this table.
60 */
61 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>();
62 private CellEditorRemover editorRemover;
63
64 /**
65 * The table has two columns. The first column is used for editing rendering and
66 * editing tag keys, the second for rendering and editing tag values.
67 *
68 */
69 static class TagTableColumnModel extends DefaultTableColumnModel {
70 public TagTableColumnModel(DefaultListSelectionModel selectionModel) {
71 setSelectionModel(selectionModel);
72 TableColumn col = null;
73 TagCellRenderer renderer = new TagCellRenderer();
74
75 // column 0 - tag key
76 col = new TableColumn(0);
77 col.setHeaderValue(tr("Key"));
78 col.setResizable(true);
79 col.setCellRenderer(renderer);
80 addColumn(col);
81
82 // column 1 - tag value
83 col = new TableColumn(1);
84 col.setHeaderValue(tr("Value"));
85 col.setResizable(true);
86 col.setCellRenderer(renderer);
87 addColumn(col);
88 }
89 }
90
91 /**
92 * Action to be run when the user navigates to the next cell in the table,
93 * for instance by pressing TAB or ENTER. The action alters the standard
94 * navigation path from cell to cell:
95 * <ul>
96 * <li>it jumps over cells in the first column</li>
97 * <li>it automatically add a new empty row when the user leaves the
98 * last cell in the table</li>
99 * <ul>
100 *
101 */
102 class SelectNextColumnCellAction extends AbstractAction {
103 public void actionPerformed(ActionEvent e) {
104 run();
105 }
106
107 public void run() {
108 int col = getSelectedColumn();
109 int row = getSelectedRow();
110 if (getCellEditor() != null) {
111 getCellEditor().stopCellEditing();
112 }
113
114 if (col == 0) {
115 col++;
116 } else if (col == 1 && row < getRowCount()-1) {
117 col=0;
118 row++;
119 } else if (col == 1 && row == getRowCount()-1){
120 // we are at the end. Append an empty row and move the focus
121 // to its second column
122 TagEditorModel model = (TagEditorModel)getModel();
123 model.appendNewTag();
124 col=0;
125 row++;
126 }
127 changeSelection(row, col, false, false);
128 requestFocusInCell(row,col);
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 requestFocusInCell(row,col);
156 }
157 }
158
159 /**
160 * Action to be run when the user invokes a delete action on the table, for
161 * instance by pressing DEL.
162 *
163 * Depending on the shape on the current selection the action deletes individual
164 * values or entire tags from the model.
165 *
166 * If the current selection consists of cells in the second column only, the keys of
167 * the selected tags are set to the empty string.
168 *
169 * If the current selection consists of cell in the third column only, the values of the
170 * selected tags are set to the empty string.
171 *
172 * If the current selection consists of cells in the second and the third column,
173 * the selected tags are removed from the model.
174 *
175 * This action listens to the table selection. It becomes enabled when the selection
176 * is non-empty, otherwise it is disabled.
177 *
178 *
179 */
180 class DeleteAction extends RunnableAction implements ListSelectionListener {
181
182 public DeleteAction() {
183 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
184 putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
185 getSelectionModel().addListSelectionListener(this);
186 getColumnModel().getSelectionModel().addListSelectionListener(this);
187 updateEnabledState();
188 }
189
190 /**
191 * delete a selection of tag names
192 */
193 protected void deleteTagNames() {
194 int[] rows = getSelectedRows();
195 TagEditorModel model = (TagEditorModel)getModel();
196 model.deleteTagNames(rows);
197 }
198
199 /**
200 * delete a selection of tag values
201 */
202 protected void deleteTagValues() {
203 int[] rows = getSelectedRows();
204 TagEditorModel model = (TagEditorModel)getModel();
205 model.deleteTagValues(rows);
206 }
207
208 /**
209 * delete a selection of tags
210 */
211 protected void deleteTags() {
212 int[] rows = getSelectedRows();
213 TagEditorModel model = (TagEditorModel)getModel();
214 model.deleteTags(rows);
215 }
216
217 @Override
218 public void run() {
219 if (!isEnabled())
220 return;
221 switch(getSelectedColumnCount()) {
222 case 1:
223 if (getSelectedColumn() == 0) {
224 deleteTagNames();
225 } else if (getSelectedColumn() == 1) {
226 deleteTagValues();
227 }
228 break;
229 case 2:
230 deleteTags();
231 break;
232 }
233
234 if (isEditing()) {
235 CellEditor editor = getCellEditor();
236 if (editor != null) {
237 editor.cancelCellEditing();
238 }
239 }
240
241 TagEditorModel model = (TagEditorModel)getModel();
242 if (model.getRowCount() == 0) {
243 model.ensureOneTag();
244 requestFocusInCell(0, 0);
245 }
246 }
247
248 /**
249 * listens to the table selection model
250 */
251 public void valueChanged(ListSelectionEvent e) {
252 updateEnabledState();
253 }
254
255 protected void updateEnabledState() {
256 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
257 setEnabled(true);
258 } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
259 setEnabled(true);
260 } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
261 setEnabled(true);
262 } else {
263 setEnabled(false);
264 }
265 }
266 }
267
268 /**
269 * Action to be run when the user adds a new tag.
270 *
271 *
272 */
273 class AddAction extends RunnableAction implements PropertyChangeListener{
274 public AddAction() {
275 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
276 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
277 TagTable.this.addPropertyChangeListener(this);
278 updateEnabledState();
279 }
280
281 @Override
282 public void run() {
283 CellEditor editor = getCellEditor();
284 if (editor != null) {
285 getCellEditor().stopCellEditing();
286 }
287 ((TagEditorModel)getModel()).appendNewTag();
288 final int rowIdx = getModel().getRowCount()-1;
289 requestFocusInCell(rowIdx, 0);
290 }
291
292 protected void updateEnabledState() {
293 setEnabled(TagTable.this.isEnabled());
294 }
295
296 public void propertyChange(PropertyChangeEvent evt) {
297 updateEnabledState();
298 }
299 }
300
301 /** the delete action */
302 private RunnableAction deleteAction = null;
303
304 /** the add action */
305 private RunnableAction addAction = null;
306
307 /**
308 *
309 * @return the delete action used by this table
310 */
311 public RunnableAction getDeleteAction() {
312 return deleteAction;
313 }
314
315 public RunnableAction getAddAction() {
316 return addAction;
317 }
318
319 /**
320 * initialize the table
321 */
322 protected void init() {
323 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
324 //setCellSelectionEnabled(true);
325 setRowSelectionAllowed(true);
326 setColumnSelectionAllowed(true);
327 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
328
329 // make ENTER behave like TAB
330 //
331 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
332 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
333
334 // install custom navigation actions
335 //
336 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
337 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
338
339 // create a delete action. Installing this action in the input and action map
340 // didn't work. We therefore handle delete requests in processKeyBindings(...)
341 //
342 deleteAction = new DeleteAction();
343
344 // create the add action
345 //
346 addAction = new AddAction();
347 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
348 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
349 getActionMap().put("addTag", addAction);
350
351 // create the table cell editor and set it to key and value columns
352 //
353 setTagCellEditor(new TagCellEditor());
354 }
355
356 /**
357 * Creates a new tag table
358 *
359 * @param model the tag editor model
360 */
361 public TagTable(TagEditorModel model) {
362 super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
363 init();
364 }
365
366 @Override
367 public Dimension getPreferredSize(){
368 Container c = getParent();
369 while(c != null && ! (c instanceof JViewport)) {
370 c = c.getParent();
371 }
372 if (c != null) {
373 Dimension d = super.getPreferredSize();
374 d.width = c.getSize().width;
375 return d;
376 }
377 return super.getPreferredSize();
378 }
379
380 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
381 int condition, boolean pressed) {
382
383 // handle delete key
384 //
385 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
386 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
387 // if DEL was pressed and only the currently edited cell is selected,
388 // don't run the delete action. DEL is handled by the CellEditor as normal
389 // DEL in the text input.
390 //
391 return super.processKeyBinding(ks, e, condition, pressed);
392 getDeleteAction().run();
393 }
394 return super.processKeyBinding(ks, e, condition, pressed);
395 }
396
397 /**
398 * @param autoCompletionList
399 */
400 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
401 if (autoCompletionList == null)
402 return;
403 if (editor != null) {
404 editor.setAutoCompletionList(autoCompletionList);
405 }
406 }
407
408 public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
409 if (autocomplete == null) {
410 logger.warning("argument autocomplete should not be null. Aborting.");
411 Thread.dumpStack();
412 return;
413 }
414 if (editor != null) {
415 editor.setAutoCompletionManager(autocomplete);
416 }
417 }
418
419 public AutoCompletionList getAutoCompletionList() {
420 if (editor != null)
421 return editor.getAutoCompletionList();
422 else
423 return null;
424 }
425
426 public TagCellEditor getTableCellEditor() {
427 return editor;
428 }
429
430 public void addOKAccelatorListener(KeyListener l) {
431 addKeyListener(l);
432 if (editor == null) {
433 logger.warning("editor is null. cannot register OK accelator listener.");
434 } else {
435 editor.getEditor().addKeyListener(l);
436 }
437 }
438
439 /**
440 * Inject a tag cell editor in the tag table
441 *
442 * @param editor
443 */
444 public void setTagCellEditor(TagCellEditor editor) {
445 if (isEditing()) {
446 this.editor.cancelCellEditing();
447 }
448 this.editor = editor;
449 getColumnModel().getColumn(0).setCellEditor(editor);
450 getColumnModel().getColumn(1).setCellEditor(editor);
451 }
452
453 public void requestFocusInCell(final int row, final int col) {
454
455 // the following code doesn't work reliably. If a table cell
456 // gains focus using editCellAt() and requestFocusInWindow()
457 // it isn't possible to tab to the next table cell using TAB or
458 // ENTER. Don't know why.
459 //
460 // tblTagEditor.editCellAt(row, col);
461 // if (tblTagEditor.getEditorComponent() != null) {
462 // tblTagEditor.getEditorComponent().requestFocusInWindow();
463 // }
464
465 // this is a workaround. We move the focus to the respective cell
466 // using a simulated mouse click. In this case one can tab out of
467 // the cell using TAB and ENTER.
468 //
469 Rectangle r = getCellRect(row,col, false);
470 Point p = new Point(r.x + r.width/2, r.y + r.height/2);
471 SwingUtilities.convertPointToScreen(p, this);
472 Point before = MouseInfo.getPointerInfo().getLocation();
473
474 try {
475 Robot robot = new Robot();
476 robot.mouseMove(p.x,p.y);
477 robot.mousePress(InputEvent.BUTTON1_MASK);
478 robot.mouseRelease(InputEvent.BUTTON1_MASK);
479 robot.mouseMove(before.x, before.y);
480 } catch(AWTException e) {
481 logger.log(Level.SEVERE, "failed to simulate mouse click event at (" + r.x + "," + r.y + "). Exception: " + e.toString());
482 return;
483 }
484 }
485
486 public void addComponentNotStoppingCellEditing(Component component) {
487 if (component == null) return;
488 doNotStopCellEditingWhenFocused.addIfAbsent(component);
489 }
490
491 public void removeComponentNotStoppingCellEditing(Component component) {
492 if (component == null) return;
493 doNotStopCellEditingWhenFocused.remove(component);
494 }
495
496 @Override
497 public boolean editCellAt(int row, int column, EventObject e){
498
499 // a snipped copied from the Java 1.5 implementation of JTable
500 //
501 if (cellEditor != null && !cellEditor.stopCellEditing())
502 return false;
503
504 if (row < 0 || row >= getRowCount() ||
505 column < 0 || column >= getColumnCount())
506 return false;
507
508 if (!isCellEditable(row, column))
509 return false;
510
511 // make sure our custom implementation of CellEditorRemover is created
512 if (editorRemover == null) {
513 KeyboardFocusManager fm =
514 KeyboardFocusManager.getCurrentKeyboardFocusManager();
515 editorRemover = new CellEditorRemover(fm);
516 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
517 }
518
519 // delegate to the default implementation
520 return super.editCellAt(row, column,e);
521 }
522
523
524 @Override
525 public void removeEditor() {
526 // make sure we unregister our custom implementation of CellEditorRemover
527 KeyboardFocusManager.getCurrentKeyboardFocusManager().
528 removePropertyChangeListener("permanentFocusOwner", editorRemover);
529 editorRemover = null;
530 super.removeEditor();
531 }
532
533 @Override
534 public void removeNotify() {
535 // make sure we unregister our custom implementation of CellEditorRemover
536 KeyboardFocusManager.getCurrentKeyboardFocusManager().
537 removePropertyChangeListener("permanentFocusOwner", editorRemover);
538 editorRemover = null;
539 super.removeNotify();
540 }
541
542 /**
543 * This is a custom implementation of the CellEditorRemover used in JTable
544 * to handle the client property <tt>terminateEditOnFocusLost</tt>.
545 *
546 * This implementation also checks whether focus is transferred to one of a list
547 * of dedicated components, see {@see TagTable#doNotStopCellEditingWhenFocused}.
548 * A typical example for such a component is a button in {@see TagEditorPanel}
549 * which isn't a child component of {@see TagTable} but which should respond to
550 * to focus transfer in a similar way to a child of TagTable.
551 *
552 */
553 class CellEditorRemover implements PropertyChangeListener {
554 KeyboardFocusManager focusManager;
555
556 public CellEditorRemover(KeyboardFocusManager fm) {
557 this.focusManager = fm;
558 }
559
560 public void propertyChange(PropertyChangeEvent ev) {
561 if (!isEditing())
562 return;
563
564 Component c = focusManager.getPermanentFocusOwner();
565 while (c != null) {
566 if (c == TagTable.this)
567 // focus remains inside the table
568 return;
569 if (doNotStopCellEditingWhenFocused.contains(c))
570 // focus remains on one of the associated components
571 return;
572 else if ((c instanceof Window) ||
573 (c instanceof Applet && c.getParent() == null)) {
574 if (c == SwingUtilities.getRoot(TagTable.this)) {
575 if (!getCellEditor().stopCellEditing()) {
576 getCellEditor().cancelCellEditing();
577 }
578 }
579 break;
580 }
581 c = c.getParent();
582 }
583 }
584 }
585}
Note: See TracBrowser for help on using the repository browser.