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

Last change on this file since 4310 was 4310, checked in by stoecker, 13 years ago

fix #6680, fix #6677 - i18n issues

  • Property svn:eol-style set to native
File size: 19.3 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;
25
26import javax.swing.AbstractAction;
27import javax.swing.CellEditor;
28import javax.swing.DefaultListSelectionModel;
29import javax.swing.JComponent;
30import javax.swing.JTable;
31import javax.swing.JViewport;
32import javax.swing.KeyStroke;
33import javax.swing.ListSelectionModel;
34import javax.swing.SwingUtilities;
35import javax.swing.event.ListSelectionEvent;
36import javax.swing.event.ListSelectionListener;
37import javax.swing.table.DefaultTableColumnModel;
38import javax.swing.table.TableColumn;
39
40import org.openstreetmap.josm.gui.dialogs.relation.RunnableAction;
41import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
42import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
43import org.openstreetmap.josm.tools.ImageProvider;
44
45/**
46 * This is the tabular editor component for OSM tags.
47 *
48 */
49public class TagTable extends JTable {
50 /** the table cell editor used by this table */
51 private TagCellEditor editor = null;
52
53 /** a list of components to which focus can be transferred without stopping
54 * cell editing this table.
55 */
56 private final CopyOnWriteArrayList<Component> doNotStopCellEditingWhenFocused = new CopyOnWriteArrayList<Component>();
57 private CellEditorRemover editorRemover;
58
59 /**
60 * The table has two columns. The first column is used for editing rendering and
61 * editing tag keys, the second for rendering and editing tag values.
62 *
63 */
64 static class TagTableColumnModel extends DefaultTableColumnModel {
65 public TagTableColumnModel(DefaultListSelectionModel selectionModel) {
66 setSelectionModel(selectionModel);
67 TableColumn col = null;
68 TagCellRenderer renderer = new TagCellRenderer();
69
70 // column 0 - tag key
71 col = new TableColumn(0);
72 col.setHeaderValue(tr("Key"));
73 col.setResizable(true);
74 col.setCellRenderer(renderer);
75 addColumn(col);
76
77 // column 1 - tag value
78 col = new TableColumn(1);
79 col.setHeaderValue(tr("Value"));
80 col.setResizable(true);
81 col.setCellRenderer(renderer);
82 addColumn(col);
83 }
84 }
85
86 /**
87 * Action to be run when the user navigates to the next cell in the table,
88 * for instance by pressing TAB or ENTER. The action alters the standard
89 * navigation path from cell to cell:
90 * <ul>
91 * <li>it jumps over cells in the first column</li>
92 * <li>it automatically add a new empty row when the user leaves the
93 * last cell in the table</li>
94 * <ul>
95 *
96 */
97 class SelectNextColumnCellAction extends AbstractAction {
98 public void actionPerformed(ActionEvent e) {
99 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 {@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.