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

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

see #8465 - use diamond operator where applicable

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