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

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

checkstyle

  • Property svn:eol-style set to native
File size: 21.9 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;
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 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 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 class AddAction extends RunnableAction implements PropertyChangeListener {
286 AddAction() {
287 putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
288 putValue(SHORT_DESCRIPTION, tr("Add a new tag"));
289 TagTable.this.addPropertyChangeListener(this);
290 updateEnabledState();
291 }
292
293 @Override
294 public void run() {
295 CellEditor editor = getCellEditor();
296 if (editor != null) {
297 getCellEditor().stopCellEditing();
298 }
299 final int rowIdx = model.getRowCount()-1;
300 String key = ((TagModel) model.getValueAt(rowIdx, 0)).getName();
301 if (!key.trim().isEmpty()) {
302 model.appendNewTag();
303 }
304 requestFocusInCell(model.getRowCount()-1, 0);
305 }
306
307 protected final void updateEnabledState() {
308 setEnabled(TagTable.this.isEnabled());
309 }
310
311 @Override
312 public void propertyChange(PropertyChangeEvent evt) {
313 updateEnabledState();
314 }
315 }
316
317 /**
318 * Action to be run when the user wants to paste tags from buffer
319 */
320 class PasteAction extends RunnableAction implements PropertyChangeListener {
321 PasteAction() {
322 putValue(SMALL_ICON, ImageProvider.get("", "pastetags"));
323 putValue(SHORT_DESCRIPTION, tr("Paste tags from buffer"));
324 TagTable.this.addPropertyChangeListener(this);
325 updateEnabledState();
326 }
327
328 @Override
329 public void run() {
330 Relation relation = new Relation();
331 model.applyToPrimitive(relation);
332
333 String buf = Utils.getClipboardContent();
334 if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
335 List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
336 if (directlyAdded == null || directlyAdded.isEmpty()) return;
337 PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded,
338 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;
369
370 /** the add action */
371 private RunnableAction addAction;
372
373 /** the tag paste action */
374 private RunnableAction pasteAction;
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 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
395 */
396 protected final void init(final int maxCharacters) {
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(maxCharacters);
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 * @param maxCharacters maximum number of characters allowed for keys and values, 0 for unlimited
438 */
439 public TagTable(TagEditorModel model, final int maxCharacters) {
440 super(model, new TagTableColumnModel(model.getColumnSelectionModel()), model.getRowSelectionModel());
441 this.model = model;
442 init(maxCharacters);
443 }
444
445 @Override
446 public Dimension getPreferredSize() {
447 Container c = getParent();
448 while (c != null && !(c instanceof JViewport)) {
449 c = c.getParent();
450 }
451 if (c != null) {
452 Dimension d = super.getPreferredSize();
453 d.width = c.getSize().width;
454 return d;
455 }
456 return super.getPreferredSize();
457 }
458
459 @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
460 int condition, boolean pressed) {
461
462 // handle delete key
463 //
464 if (e.getKeyCode() == KeyEvent.VK_DELETE) {
465 if (isEditing() && getSelectedColumnCount() == 1 && getSelectedRowCount() == 1)
466 // if DEL was pressed and only the currently edited cell is selected,
467 // don't run the delete action. DEL is handled by the CellEditor as normal
468 // DEL in the text input.
469 //
470 return super.processKeyBinding(ks, e, condition, pressed);
471 getDeleteAction().run();
472 }
473 return super.processKeyBinding(ks, e, condition, pressed);
474 }
475
476 /**
477 * Sets the editor autocompletion list
478 * @param autoCompletionList autocompletion list
479 */
480 public void setAutoCompletionList(AutoCompletionList autoCompletionList) {
481 if (autoCompletionList == null)
482 return;
483 if (editor != null) {
484 editor.setAutoCompletionList(autoCompletionList);
485 }
486 }
487
488 public void setAutoCompletionManager(AutoCompletionManager autocomplete) {
489 if (autocomplete == null) {
490 Main.warn("argument autocomplete should not be null. Aborting.");
491 Thread.dumpStack();
492 return;
493 }
494 if (editor != null) {
495 editor.setAutoCompletionManager(autocomplete);
496 }
497 }
498
499 public AutoCompletionList getAutoCompletionList() {
500 if (editor != null)
501 return editor.getAutoCompletionList();
502 else
503 return null;
504 }
505
506 public void setNextFocusComponent(Component nextFocusComponent) {
507 this.nextFocusComponent = nextFocusComponent;
508 }
509
510 public TagCellEditor getTableCellEditor() {
511 return editor;
512 }
513
514 public void addOKAccelatorListener(KeyListener l) {
515 addKeyListener(l);
516 if (editor != null) {
517 editor.getEditor().addKeyListener(l);
518 }
519 }
520
521 /**
522 * Inject a tag cell editor in the tag table
523 *
524 * @param editor tag cell editor
525 */
526 public void setTagCellEditor(TagCellEditor editor) {
527 if (isEditing()) {
528 this.editor.cancelCellEditing();
529 }
530 this.editor = editor;
531 getColumnModel().getColumn(0).setCellEditor(editor);
532 getColumnModel().getColumn(1).setCellEditor(editor);
533 }
534
535 public void requestFocusInCell(final int row, final int col) {
536 changeSelection(row, col, false, false);
537 editCellAt(row, col);
538 Component c = getEditorComponent();
539 if (c != null) {
540 c.requestFocusInWindow();
541 if (c instanceof JTextComponent) {
542 ((JTextComponent) c).selectAll();
543 }
544 }
545 // there was a bug here - on older 1.6 Java versions Tab was not working
546 // after such activation. In 1.7 it works OK,
547 // previous solution of using awt.Robot was resetting mouse speed on Windows
548 }
549
550 public void addComponentNotStoppingCellEditing(Component component) {
551 if (component == null) return;
552 doNotStopCellEditingWhenFocused.addIfAbsent(component);
553 }
554
555 public void removeComponentNotStoppingCellEditing(Component component) {
556 if (component == null) return;
557 doNotStopCellEditingWhenFocused.remove(component);
558 }
559
560 @Override
561 public boolean editCellAt(int row, int column, EventObject e) {
562
563 // a snipped copied from the Java 1.5 implementation of JTable
564 //
565 if (cellEditor != null && !cellEditor.stopCellEditing())
566 return false;
567
568 if (row < 0 || row >= getRowCount() ||
569 column < 0 || column >= getColumnCount())
570 return false;
571
572 if (!isCellEditable(row, column))
573 return false;
574
575 // make sure our custom implementation of CellEditorRemover is created
576 if (editorRemover == null) {
577 KeyboardFocusManager fm =
578 KeyboardFocusManager.getCurrentKeyboardFocusManager();
579 editorRemover = new CellEditorRemover(fm);
580 fm.addPropertyChangeListener("permanentFocusOwner", editorRemover);
581 }
582
583 // delegate to the default implementation
584 return super.editCellAt(row, column, e);
585 }
586
587 @Override
588 public void removeEditor() {
589 // make sure we unregister our custom implementation of CellEditorRemover
590 KeyboardFocusManager.getCurrentKeyboardFocusManager().
591 removePropertyChangeListener("permanentFocusOwner", editorRemover);
592 editorRemover = null;
593 super.removeEditor();
594 }
595
596 @Override
597 public void removeNotify() {
598 // make sure we unregister our custom implementation of CellEditorRemover
599 KeyboardFocusManager.getCurrentKeyboardFocusManager().
600 removePropertyChangeListener("permanentFocusOwner", editorRemover);
601 editorRemover = null;
602 super.removeNotify();
603 }
604
605 /**
606 * This is a custom implementation of the CellEditorRemover used in JTable
607 * to handle the client property <tt>terminateEditOnFocusLost</tt>.
608 *
609 * This implementation also checks whether focus is transferred to one of a list
610 * of dedicated components, see {@link TagTable#doNotStopCellEditingWhenFocused}.
611 * A typical example for such a component is a button in {@link TagEditorPanel}
612 * which isn't a child component of {@link TagTable} but which should respond to
613 * to focus transfer in a similar way to a child of TagTable.
614 *
615 */
616 class CellEditorRemover implements PropertyChangeListener {
617 private KeyboardFocusManager focusManager;
618
619 CellEditorRemover(KeyboardFocusManager fm) {
620 this.focusManager = fm;
621 }
622
623 @Override
624 public void propertyChange(PropertyChangeEvent ev) {
625 if (!isEditing())
626 return;
627
628 Component c = focusManager.getPermanentFocusOwner();
629 while (c != null) {
630 if (c == TagTable.this)
631 // focus remains inside the table
632 return;
633 if (doNotStopCellEditingWhenFocused.contains(c))
634 // focus remains on one of the associated components
635 return;
636 else if (c instanceof Window) {
637 if (c == SwingUtilities.getRoot(TagTable.this)) {
638 if (!getCellEditor().stopCellEditing()) {
639 getCellEditor().cancelCellEditing();
640 }
641 }
642 break;
643 }
644 c = c.getParent();
645 }
646 }
647 }
648}
Note: See TracBrowser for help on using the repository browser.