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

Last change on this file since 8285 was 8285, checked in by Don-vip, 9 years ago

fix sonar squid:S2039 - Member variable visibility should be specified

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