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

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

checkstyle: enable relevant whitespace checks and fix them

  • Property svn:eol-style set to native
File size: 21.7 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 transient 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 to its second column
132 String key = ((TagModel) model.getValueAt(row, 0)).getName();
133 if (!key.trim().isEmpty()) {
134 model.appendNewTag();
135 col = 0;
136 row++;
137 } else {
138 clearSelection();
139 if (nextFocusComponent != null)
140 nextFocusComponent.requestFocusInWindow();
141 return;
142 }
143 }
144 requestFocusInCell(row, col);
145 }
146 }
147
148 /**
149 * Action to be run when the user navigates to the previous cell in the table,
150 * for instance by pressing Shift-TAB
151 *
152 */
153 class SelectPreviousColumnCellAction extends AbstractAction {
154
155 @Override
156 public void actionPerformed(ActionEvent e) {
157 int col = getSelectedColumn();
158 int row = getSelectedRow();
159 if (getCellEditor() != null) {
160 getCellEditor().stopCellEditing();
161 }
162
163 if (col <= 0 && row <= 0) {
164 // change nothing
165 } else if (col == 1) {
166 col--;
167 } else {
168 col = 1;
169 row--;
170 }
171 requestFocusInCell(row, col);
172 }
173 }
174
175 /**
176 * Action to be run when the user invokes a delete action on the table, for
177 * instance by pressing DEL.
178 *
179 * Depending on the shape on the current selection the action deletes individual
180 * values or entire tags from the model.
181 *
182 * If the current selection consists of cells in the second column only, the keys of
183 * the selected tags are set to the empty string.
184 *
185 * If the current selection consists of cell in the third column only, the values of the
186 * selected tags are set to the empty string.
187 *
188 * If the current selection consists of cells in the second and the third column,
189 * the selected tags are removed from the model.
190 *
191 * This action listens to the table selection. It becomes enabled when the selection
192 * is non-empty, otherwise it is disabled.
193 *
194 *
195 */
196 class DeleteAction extends RunnableAction implements ListSelectionListener {
197
198 public DeleteAction() {
199 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
200 putValue(SHORT_DESCRIPTION, tr("Delete the selection in the tag table"));
201 getSelectionModel().addListSelectionListener(this);
202 getColumnModel().getSelectionModel().addListSelectionListener(this);
203 updateEnabledState();
204 }
205
206 /**
207 * delete a selection of tag names
208 */
209 protected void deleteTagNames() {
210 int[] rows = getSelectedRows();
211 model.deleteTagNames(rows);
212 }
213
214 /**
215 * delete a selection of tag values
216 */
217 protected void deleteTagValues() {
218 int[] rows = getSelectedRows();
219 model.deleteTagValues(rows);
220 }
221
222 /**
223 * delete a selection of tags
224 */
225 protected void deleteTags() {
226 int[] rows = getSelectedRows();
227 model.deleteTags(rows);
228 }
229
230 @Override
231 public void run() {
232 if (!isEnabled())
233 return;
234 switch(getSelectedColumnCount()) {
235 case 1:
236 if (getSelectedColumn() == 0) {
237 deleteTagNames();
238 } else if (getSelectedColumn() == 1) {
239 deleteTagValues();
240 }
241 break;
242 case 2:
243 deleteTags();
244 break;
245 }
246
247 if (isEditing()) {
248 CellEditor editor = getCellEditor();
249 if (editor != null) {
250 editor.cancelCellEditing();
251 }
252 }
253
254 if (model.getRowCount() == 0) {
255 model.ensureOneTag();
256 requestFocusInCell(0, 0);
257 }
258 }
259
260 /**
261 * listens to the table selection model
262 */
263 @Override
264 public void valueChanged(ListSelectionEvent e) {
265 updateEnabledState();
266 }
267
268 protected final void updateEnabledState() {
269 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
270 setEnabled(true);
271 } else if (!isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1) {
272 setEnabled(true);
273 } else if (getSelectedColumnCount() > 1 || getSelectedRowCount() > 1) {
274 setEnabled(true);
275 } else {
276 setEnabled(false);
277 }
278 }
279 }
280
281 /**
282 * Action to be run when the user adds a new tag.
283 *
284 *
285 */
286 class AddAction extends RunnableAction implements PropertyChangeListener{
287 public AddAction() {
288 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
289 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
290 TagTable.this.addPropertyChangeListener(this);
291 updateEnabledState();
292 }
293
294 @Override
295 public void run() {
296 CellEditor editor = getCellEditor();
297 if (editor != null) {
298 getCellEditor().stopCellEditing();
299 }
300 final int rowIdx = model.getRowCount()-1;
301 String key = ((TagModel) model.getValueAt(rowIdx, 0)).getName();
302 if (!key.trim().isEmpty()) {
303 model.appendNewTag();
304 }
305 requestFocusInCell(model.getRowCount()-1, 0);
306 }
307
308 protected final void updateEnabledState() {
309 setEnabled(TagTable.this.isEnabled());
310 }
311
312 @Override
313 public void propertyChange(PropertyChangeEvent evt) {
314 updateEnabledState();
315 }
316 }
317
318 /**
319 * Action to be run when the user wants to paste tags from buffer
320 */
321 class PasteAction extends RunnableAction implements PropertyChangeListener{
322 public PasteAction() {
323 putValue(SMALL_ICON, ImageProvider.get("", "pastetags"));
324 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
325 TagTable.this.addPropertyChangeListener(this);
326 updateEnabledState();
327 }
328
329 @Override
330 public void run() {
331 Relation relation = new Relation();
332 model.applyToPrimitive(relation);
333
334 String buf = Utils.getClipboardContent();
335 if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
336 List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
337 if (directlyAdded == null || directlyAdded.isEmpty()) return;
338 PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, Collections.<OsmPrimitive>singletonList(relation));
339 model.updateTags(tagPaster.execute());
340 } else {
341 // Paste tags from arbitrary text
342 Map<String, String> tags = TextTagParser.readTagsFromText(buf);
343 if (tags == null || tags.isEmpty()) {
344 TextTagParser.showBadBufferMessage(ht("/Action/PasteTags"));
345 } else if (TextTagParser.validateTags(tags)) {
346 List<Tag> newTags = new ArrayList<>();
347 for (Map.Entry<String, String> entry: tags.entrySet()) {
348 String k = entry.getKey();
349 String v = entry.getValue();
350 newTags.add(new Tag(k, v));
351 }
352 model.updateTags(newTags);
353 }
354 }
355 }
356
357 protected final void updateEnabledState() {
358 setEnabled(TagTable.this.isEnabled());
359 }
360
361 @Override
362 public void propertyChange(PropertyChangeEvent evt) {
363 updateEnabledState();
364 }
365 }
366
367 /** the delete action */
368 private RunnableAction deleteAction = null;
369
370 /** the add action */
371 private RunnableAction addAction = null;
372
373 /** the tag paste action */
374 private RunnableAction pasteAction = null;
375
376 /**
377 *
378 * @return the delete action used by this table
379 */
380 public RunnableAction getDeleteAction() {
381 return deleteAction;
382 }
383
384 public RunnableAction getAddAction() {
385 return addAction;
386 }
387
388 public RunnableAction getPasteAction() {
389 return pasteAction;
390 }
391
392 /**
393 * initialize the table
394 */
395 protected final void init() {
396 setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
397 setRowSelectionAllowed(true);
398 setColumnSelectionAllowed(true);
399 setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
400
401 // make ENTER behave like TAB
402 //
403 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
404 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false), "selectNextColumnCell");
405
406 // install custom navigation actions
407 //
408 getActionMap().put("selectNextColumnCell", new SelectNextColumnCellAction());
409 getActionMap().put("selectPreviousColumnCell", new SelectPreviousColumnCellAction());
410
411 // create a delete action. Installing this action in the input and action map
412 // didn't work. We therefore handle delete requests in processKeyBindings(...)
413 //
414 deleteAction = new DeleteAction();
415
416 // create the add action
417 //
418 addAction = new AddAction();
419 getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
420 .put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.CTRL_MASK), "addTag");
421 getActionMap().put("addTag", addAction);
422
423 pasteAction = new PasteAction();
424
425 // create the table cell editor and set it to key and value columns
426 //
427 TagCellEditor tmpEditor = new TagCellEditor();
428 setRowHeight(tmpEditor.getEditor().getPreferredSize().height);
429 setTagCellEditor(tmpEditor);
430 }
431
432 /**
433 * Creates a new tag table
434 *
435 * @param model the tag editor model
436 */
437 public TagTable(TagEditorModel model) {
438 super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
439 this.model = model;
440 init();
441 }
442
443 @Override
444 public Dimension getPreferredSize() {
445 Container c = getParent();
446 while (c != null && !(c instanceof JViewport)) {
447 c = c.getParent();
448 }
449 if (c != null) {
450 Dimension d = super.getPreferredSize();
451 d.width = c.getSize().width;
452 return d;
453 }
454 return super.getPreferredSize();
455 }
456
457 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
458 int condition, boolean pressed) {
459
460 // handle delete key
461 //
462 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
463 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
464 // if DEL was pressed and only the currently edited cell is selected,
465 // don't run the delete action. DEL is handled by the CellEditor as normal
466 // DEL in the text input.
467 //
468 return super.processKeyBinding(ks, e, condition, pressed);
469 getDeleteAction().run();
470 }
471 return super.processKeyBinding(ks, e, condition, pressed);
472 }
473
474 /**
475 * Sets the editor autocompletion list
476 * @param autoCompletionList autocompletion list
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 tag cell 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 using 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 @Override
586 public void removeEditor() {
587 // make sure we unregister our custom implementation of CellEditorRemover
588 KeyboardFocusManager.getCurrentKeyboardFocusManager().
589 removePropertyChangeListener("permanentFocusOwner", editorRemover);
590 editorRemover = null;
591 super.removeEditor();
592 }
593
594 @Override
595 public void removeNotify() {
596 // make sure we unregister our custom implementation of CellEditorRemover
597 KeyboardFocusManager.getCurrentKeyboardFocusManager().
598 removePropertyChangeListener("permanentFocusOwner", editorRemover);
599 editorRemover = null;
600 super.removeNotify();
601 }
602
603 /**
604 * This is a custom implementation of the CellEditorRemover used in JTable
605 * to handle the client property <tt>terminateEditOnFocusLost</tt>.
606 *
607 * This implementation also checks whether focus is transferred to one of a list
608 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
609 * A typical example for such a component is a button in {@link TagEditorPanel}
610 * which isn't a child component of {@link TagTable} but which should respond to
611 * to focus transfer in a similar way to a child of TagTable.
612 *
613 */
614 class CellEditorRemover implements PropertyChangeListener {
615 private KeyboardFocusManager focusManager;
616
617 public CellEditorRemover(KeyboardFocusManager fm) {
618 this.focusManager = fm;
619 }
620
621 @Override
622 public void propertyChange(PropertyChangeEvent ev) {
623 if (!isEditing())
624 return;
625
626 Component c = focusManager.getPermanentFocusOwner();
627 while (c != null) {
628 if (c == TagTable.this)
629 // focus remains inside the table
630 return;
631 if (doNotStopCellEditingWhenFocused.contains(c))
632 // focus remains on one of the associated components
633 return;
634 else if (c instanceof Window) {
635 if (c == SwingUtilities.getRoot(TagTable.this)) {
636 if (!getCellEditor().stopCellEditing()) {
637 getCellEditor().cancelCellEditing();
638 }
639 }
640 break;
641 }
642 c = c.getParent();
643 }
644 }
645 }
646}
Note: See TracBrowser for help on using the repository browser.