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

Last change on this file since 6092 was 6092, checked in by akks, 11 years ago

fix #7352: add "Paste tags" button to the relation editor

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